mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-26 11:21:16 +01:00
Run oxfmt across repo, add format script and docs
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/action-copy-curl",
|
||||
"displayName": "Copy as Curl",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Copy request as a curl command",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/action-copy-curl"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { HttpRequest, PluginDefinition } from '@yaakapp/api';
|
||||
import type { HttpRequest, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
const NEWLINE = '\\\n ';
|
||||
const NEWLINE = "\\\n ";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
httpRequestActions: [
|
||||
{
|
||||
label: 'Copy as Curl',
|
||||
icon: 'copy',
|
||||
label: "Copy as Curl",
|
||||
icon: "copy",
|
||||
async onSelect(ctx, args) {
|
||||
const rendered_request = await ctx.httpRequest.render({
|
||||
httpRequest: args.httpRequest,
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
const data = await convertToCurl(rendered_request);
|
||||
await ctx.clipboard.copyText(data);
|
||||
await ctx.toast.show({
|
||||
message: 'Command copied to clipboard',
|
||||
icon: 'copy',
|
||||
color: 'success',
|
||||
message: "Command copied to clipboard",
|
||||
icon: "copy",
|
||||
color: "success",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -25,40 +25,40 @@ export const plugin: PluginDefinition = {
|
||||
};
|
||||
|
||||
export async function convertToCurl(request: Partial<HttpRequest>) {
|
||||
const xs = ['curl'];
|
||||
const xs = ["curl"];
|
||||
|
||||
// Add method and URL all on first line
|
||||
if (request.method) xs.push('-X', request.method);
|
||||
if (request.method) xs.push("-X", request.method);
|
||||
|
||||
// Build final URL with parameters (compatible with old curl)
|
||||
let finalUrl = request.url || '';
|
||||
let finalUrl = request.url || "";
|
||||
const urlParams = (request.urlParameters ?? []).filter(onlyEnabled);
|
||||
if (urlParams.length > 0) {
|
||||
// Build url
|
||||
const [base, hash] = finalUrl.split('#');
|
||||
const separator = base?.includes('?') ? '&' : '?';
|
||||
const [base, hash] = finalUrl.split("#");
|
||||
const separator = base?.includes("?") ? "&" : "?";
|
||||
const queryString = urlParams
|
||||
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
|
||||
.join('&');
|
||||
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
|
||||
.join("&");
|
||||
finalUrl = base + separator + queryString + (hash ? `#${hash}` : "");
|
||||
}
|
||||
|
||||
// Add API key authentication
|
||||
if (request.authenticationType === 'apikey') {
|
||||
if (request.authentication?.location === 'query') {
|
||||
const sep = finalUrl.includes('?') ? '&' : '?';
|
||||
if (request.authenticationType === "apikey") {
|
||||
if (request.authentication?.location === "query") {
|
||||
const sep = finalUrl.includes("?") ? "&" : "?";
|
||||
finalUrl = [
|
||||
finalUrl,
|
||||
sep,
|
||||
encodeURIComponent(request.authentication?.key ?? 'token'),
|
||||
'=',
|
||||
encodeURIComponent(request.authentication?.value ?? ''),
|
||||
].join('');
|
||||
encodeURIComponent(request.authentication?.key ?? "token"),
|
||||
"=",
|
||||
encodeURIComponent(request.authentication?.value ?? ""),
|
||||
].join("");
|
||||
} else {
|
||||
request.headers = request.headers ?? [];
|
||||
request.headers.push({
|
||||
name: request.authentication?.key ?? 'X-Api-Key',
|
||||
value: request.authentication?.value ?? '',
|
||||
name: request.authentication?.key ?? "X-Api-Key",
|
||||
value: request.authentication?.value ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -68,80 +68,80 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
|
||||
|
||||
// Add headers
|
||||
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
|
||||
xs.push('--header', quote(`${h.name}: ${h.value}`));
|
||||
xs.push("--header", quote(`${h.name}: ${h.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add form params
|
||||
const type = request.bodyType ?? 'none';
|
||||
const type = request.bodyType ?? "none";
|
||||
if (
|
||||
(type === 'multipart/form-data' || type === 'application/x-www-form-urlencoded') &&
|
||||
(type === "multipart/form-data" || type === "application/x-www-form-urlencoded") &&
|
||||
Array.isArray(request.body?.form)
|
||||
) {
|
||||
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
|
||||
const flag = request.bodyType === "multipart/form-data" ? "--form" : "--data";
|
||||
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
|
||||
if (p.file) {
|
||||
let v = `${p.name}=@${p.file}`;
|
||||
v += p.contentType ? `;type=${p.contentType}` : '';
|
||||
v += p.contentType ? `;type=${p.contentType}` : "";
|
||||
xs.push(flag, v);
|
||||
} else {
|
||||
xs.push(flag, quote(`${p.name}=${p.value}`));
|
||||
}
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
} else if (type === 'graphql' && typeof request.body?.query === 'string') {
|
||||
} else if (type === "graphql" && typeof request.body?.query === "string") {
|
||||
const body = {
|
||||
query: request.body.query || '',
|
||||
query: request.body.query || "",
|
||||
variables: maybeParseJSON(request.body.variables, undefined),
|
||||
};
|
||||
xs.push('--data', quote(JSON.stringify(body)));
|
||||
xs.push("--data", quote(JSON.stringify(body)));
|
||||
xs.push(NEWLINE);
|
||||
} else if (type !== 'none' && typeof request.body?.text === 'string') {
|
||||
xs.push('--data', quote(request.body.text));
|
||||
} else if (type !== "none" && typeof request.body?.text === "string") {
|
||||
xs.push("--data", quote(request.body.text));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add basic/digest authentication
|
||||
if (request.authentication?.disabled !== true) {
|
||||
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
|
||||
if (request.authenticationType === 'digest') xs.push('--digest');
|
||||
if (request.authenticationType === "basic" || request.authenticationType === "digest") {
|
||||
if (request.authenticationType === "digest") xs.push("--digest");
|
||||
xs.push(
|
||||
'--user',
|
||||
"--user",
|
||||
quote(
|
||||
`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`,
|
||||
`${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`,
|
||||
),
|
||||
);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add bearer authentication
|
||||
if (request.authenticationType === 'bearer') {
|
||||
if (request.authenticationType === "bearer") {
|
||||
const value =
|
||||
`${request.authentication?.prefix ?? 'Bearer'} ${request.authentication?.token ?? ''}`.trim();
|
||||
xs.push('--header', quote(`Authorization: ${value}`));
|
||||
`${request.authentication?.prefix ?? "Bearer"} ${request.authentication?.token ?? ""}`.trim();
|
||||
xs.push("--header", quote(`Authorization: ${value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
if (request.authenticationType === 'auth-aws-sig-v4') {
|
||||
if (request.authenticationType === "auth-aws-sig-v4") {
|
||||
xs.push(
|
||||
'--aws-sigv4',
|
||||
"--aws-sigv4",
|
||||
[
|
||||
'aws',
|
||||
'amz',
|
||||
request.authentication?.region ?? '',
|
||||
request.authentication?.service ?? '',
|
||||
].join(':'),
|
||||
"aws",
|
||||
"amz",
|
||||
request.authentication?.region ?? "",
|
||||
request.authentication?.service ?? "",
|
||||
].join(":"),
|
||||
);
|
||||
xs.push(NEWLINE);
|
||||
xs.push(
|
||||
'--user',
|
||||
"--user",
|
||||
quote(
|
||||
`${request.authentication?.accessKeyId ?? ''}:${request.authentication?.secretAccessKey ?? ''}`,
|
||||
`${request.authentication?.accessKeyId ?? ""}:${request.authentication?.secretAccessKey ?? ""}`,
|
||||
),
|
||||
);
|
||||
if (request.authentication?.sessionToken) {
|
||||
xs.push(NEWLINE);
|
||||
xs.push('--header', quote(`X-Amz-Security-Token: ${request.authentication.sessionToken}`));
|
||||
xs.push("--header", quote(`X-Amz-Security-Token: ${request.authentication.sessionToken}`));
|
||||
}
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
|
||||
xs.splice(xs.length - 1, 1);
|
||||
}
|
||||
|
||||
return xs.join(' ');
|
||||
return xs.join(" ");
|
||||
}
|
||||
|
||||
function quote(arg: string): string {
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convertToCurl } from '../src';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertToCurl } from "../src";
|
||||
|
||||
describe('exporter-curl', () => {
|
||||
test('Exports GET with params', async () => {
|
||||
describe("exporter-curl", () => {
|
||||
test("Exports GET with params", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: "a", value: "aaa" },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: false },
|
||||
],
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(' \\n '));
|
||||
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(" \\n "));
|
||||
});
|
||||
|
||||
test('Exports GET with params and hash', async () => {
|
||||
test("Exports GET with params and hash", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app/path#section',
|
||||
url: "https://yaak.app/path#section",
|
||||
urlParameters: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: "a", value: "aaa" },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: false },
|
||||
],
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(' \\n '));
|
||||
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(" \\n "));
|
||||
});
|
||||
|
||||
test('Exports POST with url form data', async () => {
|
||||
test("Exports POST with url form data", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: "a", value: "aaa" },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: false },
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(' \\\n '),
|
||||
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports POST with GraphQL data', async () => {
|
||||
test("Exports POST with GraphQL data", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'graphql',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
query: '{foo,bar}',
|
||||
query: "{foo,bar}",
|
||||
variables: '{"a": "aaa", "b": "bbb"}',
|
||||
},
|
||||
}),
|
||||
@@ -62,37 +62,37 @@ describe('exporter-curl', () => {
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--data '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`,
|
||||
].join(' \\\n '),
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports POST with GraphQL data no variables', async () => {
|
||||
test("Exports POST with GraphQL data no variables", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'graphql',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
query: '{foo,bar}',
|
||||
query: "{foo,bar}",
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(' \\\n '),
|
||||
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports PUT with multipart form', async () => {
|
||||
test("Exports PUT with multipart form", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'PUT',
|
||||
bodyType: 'multipart/form-data',
|
||||
url: "https://yaak.app",
|
||||
method: "PUT",
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: 'f', file: '/foo/bar.png', contentType: 'image/png' },
|
||||
{ name: "a", value: "aaa" },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: false },
|
||||
{ name: "f", file: "/foo/bar.png", contentType: "image/png" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -101,314 +101,314 @@ describe('exporter-curl', () => {
|
||||
`curl -X PUT 'https://yaak.app'`,
|
||||
`--form 'a=aaa'`,
|
||||
`--form 'b=bbb'`,
|
||||
'--form f=@/foo/bar.png;type=image/png',
|
||||
].join(' \\\n '),
|
||||
"--form f=@/foo/bar.png;type=image/png",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports JSON body', async () => {
|
||||
test("Exports JSON body", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/json",
|
||||
body: {
|
||||
text: `{"foo":"bar's"}`,
|
||||
},
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
headers: [{ name: "Content-Type", value: "application/json" }],
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--header 'Content-Type: application/json'`,
|
||||
`--data '{"foo":"bar\\'s"}'`,
|
||||
].join(' \\\n '),
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports multi-line JSON body', async () => {
|
||||
test("Exports multi-line JSON body", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/json",
|
||||
body: {
|
||||
text: `{"foo":"bar",\n"baz":"qux"}`,
|
||||
},
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
headers: [{ name: "Content-Type", value: "application/json" }],
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--header 'Content-Type: application/json'`,
|
||||
`--data '{"foo":"bar",\n"baz":"qux"}'`,
|
||||
].join(' \\\n '),
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports headers', async () => {
|
||||
test("Exports headers", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
headers: [
|
||||
{ name: 'a', value: 'aaa' },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: false },
|
||||
{ name: "a", value: "aaa" },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: false },
|
||||
],
|
||||
}),
|
||||
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(' \\\n '));
|
||||
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Basic auth', async () => {
|
||||
test("Basic auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Basic auth disabled', async () => {
|
||||
test("Basic auth disabled", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
disabled: true,
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Broken basic auth', async () => {
|
||||
test("Broken basic auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Digest auth', async () => {
|
||||
test("Digest auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'digest',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "digest",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Bearer auth', async () => {
|
||||
test("Bearer auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: 'tok',
|
||||
token: "tok",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Bearer auth with custom prefix', async () => {
|
||||
test("Bearer auth with custom prefix", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: 'abc123',
|
||||
prefix: 'Token',
|
||||
token: "abc123",
|
||||
prefix: "Token",
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(' \\\n '),
|
||||
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Bearer auth with empty prefix', async () => {
|
||||
test("Bearer auth with empty prefix", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: 'xyz789',
|
||||
prefix: '',
|
||||
token: "xyz789",
|
||||
prefix: "",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Broken bearer auth', async () => {
|
||||
test("Broken bearer auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'bearer',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('AWS v4 auth', async () => {
|
||||
test("AWS v4 auth", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'auth-aws-sig-v4',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "auth-aws-sig-v4",
|
||||
authentication: {
|
||||
accessKeyId: 'ak',
|
||||
secretAccessKey: 'sk',
|
||||
sessionToken: '',
|
||||
region: 'us-east-1',
|
||||
service: 's3',
|
||||
accessKeyId: "ak",
|
||||
secretAccessKey: "sk",
|
||||
sessionToken: "",
|
||||
region: "us-east-1",
|
||||
service: "s3",
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[`curl 'https://yaak.app'`, '--aws-sigv4 aws:amz:us-east-1:s3', `--user 'ak:sk'`].join(
|
||||
' \\\n ',
|
||||
[`curl 'https://yaak.app'`, "--aws-sigv4 aws:amz:us-east-1:s3", `--user 'ak:sk'`].join(
|
||||
" \\\n ",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('AWS v4 auth with session', async () => {
|
||||
test("AWS v4 auth with session", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'auth-aws-sig-v4',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "auth-aws-sig-v4",
|
||||
authentication: {
|
||||
accessKeyId: 'ak',
|
||||
secretAccessKey: 'sk',
|
||||
sessionToken: 'st',
|
||||
region: 'us-east-1',
|
||||
service: 's3',
|
||||
accessKeyId: "ak",
|
||||
secretAccessKey: "sk",
|
||||
sessionToken: "st",
|
||||
region: "us-east-1",
|
||||
service: "s3",
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl 'https://yaak.app'`,
|
||||
'--aws-sigv4 aws:amz:us-east-1:s3',
|
||||
"--aws-sigv4 aws:amz:us-east-1:s3",
|
||||
`--user 'ak:sk'`,
|
||||
`--header 'X-Amz-Security-Token: st'`,
|
||||
].join(' \\\n '),
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('API key auth header', async () => {
|
||||
test("API key auth header", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'header',
|
||||
key: 'X-Header',
|
||||
value: 'my-token',
|
||||
location: "header",
|
||||
key: "X-Header",
|
||||
value: "my-token",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Header: my-token'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Header: my-token'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth header query', async () => {
|
||||
test("API key auth header query", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app?hi=there',
|
||||
urlParameters: [{ name: 'param', value: 'hi' }],
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app?hi=there",
|
||||
urlParameters: [{ name: "param", value: "hi" }],
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
key: 'foo',
|
||||
value: 'bar',
|
||||
location: "query",
|
||||
key: "foo",
|
||||
value: "bar",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?hi=there¶m=hi&foo=bar'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app?hi=there¶m=hi&foo=bar'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth header query with params', async () => {
|
||||
test("API key auth header query with params", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [{ name: 'param', value: 'hi' }],
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [{ name: "param", value: "hi" }],
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
key: 'foo',
|
||||
value: 'bar',
|
||||
location: "query",
|
||||
key: "foo",
|
||||
value: "bar",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?param=hi&foo=bar'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app?param=hi&foo=bar'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth header default', async () => {
|
||||
test("API key auth header default", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'header',
|
||||
location: "header",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Api-Key: '`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Api-Key: '`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth query', async () => {
|
||||
test("API key auth query", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
key: 'foo',
|
||||
value: 'bar-baz',
|
||||
location: "query",
|
||||
key: "foo",
|
||||
value: "bar-baz",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar-baz'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar-baz'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth query with existing', async () => {
|
||||
test("API key auth query with existing", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app?foo=bar&baz=qux',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app?foo=bar&baz=qux",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
key: 'hi',
|
||||
value: 'there',
|
||||
location: "query",
|
||||
key: "hi",
|
||||
value: "there",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth query default', async () => {
|
||||
test("API key auth query default", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app?foo=bar&baz=qux',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app?foo=bar&baz=qux",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
location: "query",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&token='`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&token='`].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Stale body data', async () => {
|
||||
test("Stale body data", async () => {
|
||||
expect(
|
||||
await convertToCurl({
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'none',
|
||||
url: "https://yaak.app",
|
||||
bodyType: "none",
|
||||
body: {
|
||||
text: 'ignore me',
|
||||
text: "ignore me",
|
||||
},
|
||||
}),
|
||||
).toEqual([`curl 'https://yaak.app'`].join(' \\\n '));
|
||||
).toEqual([`curl 'https://yaak.app'`].join(" \\\n "));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,6 @@ The plugin analyzes your gRPC request configuration and generates a properly for
|
||||
|
||||
### Simple Unary Call
|
||||
|
||||
|
||||
```bash
|
||||
grpcurl -plaintext \
|
||||
-d '{"name": "John Doe"}' \
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/action-copy-grpcurl",
|
||||
"displayName": "Copy as gRPCurl",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Copy gRPC request as a grpcurl command",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/action-copy-grpcurl"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import path from 'node:path';
|
||||
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
|
||||
import path from "node:path";
|
||||
import type { GrpcRequest, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
const NEWLINE = '\\\n ';
|
||||
const NEWLINE = "\\\n ";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
grpcRequestActions: [
|
||||
{
|
||||
label: 'Copy as gRPCurl',
|
||||
icon: 'copy',
|
||||
label: "Copy as gRPCurl",
|
||||
icon: "copy",
|
||||
async onSelect(ctx, args) {
|
||||
const rendered_request = await ctx.grpcRequest.render({
|
||||
grpcRequest: args.grpcRequest,
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
const data = await convert(rendered_request, args.protoFiles);
|
||||
await ctx.clipboard.copyText(data);
|
||||
await ctx.toast.show({
|
||||
message: 'Command copied to clipboard',
|
||||
icon: 'copy',
|
||||
color: 'success',
|
||||
message: "Command copied to clipboard",
|
||||
icon: "copy",
|
||||
color: "success",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -26,14 +26,14 @@ export const plugin: PluginDefinition = {
|
||||
};
|
||||
|
||||
export async function convert(request: Partial<GrpcRequest>, allProtoFiles: string[]) {
|
||||
const xs = ['grpcurl'];
|
||||
const xs = ["grpcurl"];
|
||||
|
||||
if (request.url?.startsWith('http://')) {
|
||||
xs.push('-plaintext');
|
||||
if (request.url?.startsWith("http://")) {
|
||||
xs.push("-plaintext");
|
||||
}
|
||||
|
||||
const protoIncludes = allProtoFiles.filter((f) => !f.endsWith('.proto'));
|
||||
const protoFiles = allProtoFiles.filter((f) => f.endsWith('.proto'));
|
||||
const protoIncludes = allProtoFiles.filter((f) => !f.endsWith(".proto"));
|
||||
const protoFiles = allProtoFiles.filter((f) => f.endsWith(".proto"));
|
||||
|
||||
const inferredIncludes = new Set<string>();
|
||||
for (const f of protoFiles) {
|
||||
@@ -41,59 +41,59 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
|
||||
if (protoDir) {
|
||||
inferredIncludes.add(protoDir);
|
||||
} else {
|
||||
inferredIncludes.add(path.posix.join(f, '..'));
|
||||
inferredIncludes.add(path.posix.join(f, '..', '..'));
|
||||
inferredIncludes.add(path.posix.join(f, ".."));
|
||||
inferredIncludes.add(path.posix.join(f, "..", ".."));
|
||||
}
|
||||
}
|
||||
|
||||
for (const f of protoIncludes) {
|
||||
xs.push('-import-path', quote(f));
|
||||
xs.push("-import-path", quote(f));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
for (const f of inferredIncludes.values()) {
|
||||
xs.push('-import-path', quote(f));
|
||||
xs.push("-import-path", quote(f));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
for (const f of protoFiles) {
|
||||
xs.push('-proto', quote(f));
|
||||
xs.push("-proto", quote(f));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add headers
|
||||
for (const h of (request.metadata ?? []).filter(onlyEnabled)) {
|
||||
xs.push('-H', quote(`${h.name}: ${h.value}`));
|
||||
xs.push("-H", quote(`${h.name}: ${h.value}`));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add basic authentication
|
||||
if (request.authentication?.disabled !== true) {
|
||||
if (request.authenticationType === 'basic') {
|
||||
const user = request.authentication?.username ?? '';
|
||||
const pass = request.authentication?.password ?? '';
|
||||
if (request.authenticationType === "basic") {
|
||||
const user = request.authentication?.username ?? "";
|
||||
const pass = request.authentication?.password ?? "";
|
||||
const encoded = btoa(`${user}:${pass}`);
|
||||
xs.push('-H', quote(`Authorization: Basic ${encoded}`));
|
||||
xs.push("-H", quote(`Authorization: Basic ${encoded}`));
|
||||
xs.push(NEWLINE);
|
||||
} else if (request.authenticationType === 'bearer') {
|
||||
} else if (request.authenticationType === "bearer") {
|
||||
// Add bearer authentication
|
||||
xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
|
||||
xs.push("-H", quote(`Authorization: Bearer ${request.authentication?.token ?? ""}`));
|
||||
xs.push(NEWLINE);
|
||||
} else if (request.authenticationType === 'apikey') {
|
||||
if (request.authentication?.location === 'query') {
|
||||
const sep = request.url?.includes('?') ? '&' : '?';
|
||||
} else if (request.authenticationType === "apikey") {
|
||||
if (request.authentication?.location === "query") {
|
||||
const sep = request.url?.includes("?") ? "&" : "?";
|
||||
request.url = [
|
||||
request.url,
|
||||
sep,
|
||||
encodeURIComponent(request.authentication?.key ?? 'token'),
|
||||
'=',
|
||||
encodeURIComponent(request.authentication?.value ?? ''),
|
||||
].join('');
|
||||
encodeURIComponent(request.authentication?.key ?? "token"),
|
||||
"=",
|
||||
encodeURIComponent(request.authentication?.value ?? ""),
|
||||
].join("");
|
||||
} else {
|
||||
xs.push(
|
||||
'-H',
|
||||
"-H",
|
||||
quote(
|
||||
`${request.authentication?.key ?? 'X-Api-Key'}: ${request.authentication?.value ?? ''}`,
|
||||
`${request.authentication?.key ?? "X-Api-Key"}: ${request.authentication?.value ?? ""}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -103,13 +103,13 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
|
||||
|
||||
// Add form params
|
||||
if (request.message) {
|
||||
xs.push('-d', quote(request.message));
|
||||
xs.push("-d", quote(request.message));
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
|
||||
// Add the server address
|
||||
if (request.url) {
|
||||
const server = request.url.replace(/^https?:\/\//, ''); // remove protocol
|
||||
const server = request.url.replace(/^https?:\/\//, ""); // remove protocol
|
||||
xs.push(server);
|
||||
xs.push(NEWLINE);
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
|
||||
xs.splice(xs.length - 1, 1);
|
||||
}
|
||||
|
||||
return xs.join(' ');
|
||||
return xs.join(" ");
|
||||
}
|
||||
|
||||
function quote(arg: string): string {
|
||||
@@ -141,7 +141,7 @@ function findParentProtoDir(startPath: string): string | null {
|
||||
let dir = path.resolve(startPath);
|
||||
|
||||
while (true) {
|
||||
if (path.basename(dir) === 'proto') {
|
||||
if (path.basename(dir) === "proto") {
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convert } from '../src';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convert } from "../src";
|
||||
|
||||
describe('exporter-curl', () => {
|
||||
test('Simple example', async () => {
|
||||
describe("exporter-curl", () => {
|
||||
test("Simple example", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
},
|
||||
[],
|
||||
),
|
||||
).toEqual(['grpcurl yaak.app'].join(' \\\n '));
|
||||
).toEqual(["grpcurl yaak.app"].join(" \\\n "));
|
||||
});
|
||||
test('Basic metadata', async () => {
|
||||
test("Basic metadata", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
metadata: [
|
||||
{ name: 'aaa', value: 'AAA' },
|
||||
{ enabled: true, name: 'bbb', value: 'BBB' },
|
||||
{ enabled: false, name: 'disabled', value: 'ddd' },
|
||||
{ name: "aaa", value: "AAA" },
|
||||
{ enabled: true, name: "bbb", value: "BBB" },
|
||||
{ enabled: false, name: "disabled", value: "ddd" },
|
||||
],
|
||||
},
|
||||
[],
|
||||
),
|
||||
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, 'yaak.app'].join(' \\\n '));
|
||||
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, "yaak.app"].join(" \\\n "));
|
||||
});
|
||||
test('Basic auth', async () => {
|
||||
test("Basic auth", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
},
|
||||
[],
|
||||
),
|
||||
).toEqual([`grpcurl -H 'Authorization: Basic dXNlcjpwYXNz'`, 'yaak.app'].join(' \\\n '));
|
||||
).toEqual([`grpcurl -H 'Authorization: Basic dXNlcjpwYXNz'`, "yaak.app"].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth', async () => {
|
||||
test("API key auth", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
key: 'X-Token',
|
||||
value: 'tok',
|
||||
key: "X-Token",
|
||||
value: "tok",
|
||||
},
|
||||
},
|
||||
[],
|
||||
),
|
||||
).toEqual([`grpcurl -H 'X-Token: tok'`, 'yaak.app'].join(' \\\n '));
|
||||
).toEqual([`grpcurl -H 'X-Token: tok'`, "yaak.app"].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('API key auth', async () => {
|
||||
test("API key auth", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'apikey',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: 'query',
|
||||
key: 'token',
|
||||
value: 'tok 1',
|
||||
location: "query",
|
||||
key: "token",
|
||||
value: "tok 1",
|
||||
},
|
||||
},
|
||||
[],
|
||||
),
|
||||
).toEqual(['grpcurl', 'yaak.app?token=tok%201'].join(' \\\n '));
|
||||
).toEqual(["grpcurl", "yaak.app?token=tok%201"].join(" \\\n "));
|
||||
});
|
||||
|
||||
test('Single proto file', async () => {
|
||||
expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual(
|
||||
test("Single proto file", async () => {
|
||||
expect(await convert({ url: "https://yaak.app" }, ["/foo/bar/baz.proto"])).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/foo/bar'`,
|
||||
`-import-path '/foo'`,
|
||||
`-proto '/foo/bar/baz.proto'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Multiple proto files, same dir', async () => {
|
||||
test("Multiple proto files, same dir", async () => {
|
||||
expect(
|
||||
await convert({ url: 'https://yaak.app' }, ['/foo/bar/aaa.proto', '/foo/bar/bbb.proto']),
|
||||
await convert({ url: "https://yaak.app" }, ["/foo/bar/aaa.proto", "/foo/bar/bbb.proto"]),
|
||||
).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/foo/bar'`,
|
||||
`-import-path '/foo'`,
|
||||
`-proto '/foo/bar/aaa.proto'`,
|
||||
`-proto '/foo/bar/bbb.proto'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Multiple proto files, different dir', async () => {
|
||||
test("Multiple proto files, different dir", async () => {
|
||||
expect(
|
||||
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb/ccc.proto', '/xxx/yyy/zzz.proto']),
|
||||
await convert({ url: "https://yaak.app" }, ["/aaa/bbb/ccc.proto", "/xxx/yyy/zzz.proto"]),
|
||||
).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/aaa/bbb'`,
|
||||
@@ -110,23 +110,23 @@ describe('exporter-curl', () => {
|
||||
`-import-path '/xxx'`,
|
||||
`-proto '/aaa/bbb/ccc.proto'`,
|
||||
`-proto '/xxx/yyy/zzz.proto'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Single include dir', async () => {
|
||||
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb'])).toEqual(
|
||||
[`grpcurl -import-path '/aaa/bbb'`, 'yaak.app'].join(' \\\n '),
|
||||
test("Single include dir", async () => {
|
||||
expect(await convert({ url: "https://yaak.app" }, ["/aaa/bbb"])).toEqual(
|
||||
[`grpcurl -import-path '/aaa/bbb'`, "yaak.app"].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Multiple include dir', async () => {
|
||||
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy'])).toEqual(
|
||||
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, 'yaak.app'].join(' \\\n '),
|
||||
test("Multiple include dir", async () => {
|
||||
expect(await convert({ url: "https://yaak.app" }, ["/aaa/bbb", "/xxx/yyy"])).toEqual(
|
||||
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, "yaak.app"].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Mixed proto and dirs', async () => {
|
||||
test("Mixed proto and dirs", async () => {
|
||||
expect(
|
||||
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy', '/foo/bar.proto']),
|
||||
await convert({ url: "https://yaak.app" }, ["/aaa/bbb", "/xxx/yyy", "/foo/bar.proto"]),
|
||||
).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/aaa/bbb'`,
|
||||
@@ -134,45 +134,45 @@ describe('exporter-curl', () => {
|
||||
`-import-path '/foo'`,
|
||||
`-import-path '/'`,
|
||||
`-proto '/foo/bar.proto'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
test('Sends data', async () => {
|
||||
test("Sends data", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
message: JSON.stringify({ foo: 'bar', baz: 1.0 }, null, 2),
|
||||
url: "https://yaak.app",
|
||||
message: JSON.stringify({ foo: "bar", baz: 1.0 }, null, 2),
|
||||
},
|
||||
['/foo.proto'],
|
||||
["/foo.proto"],
|
||||
),
|
||||
).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/'`,
|
||||
`-proto '/foo.proto'`,
|
||||
`-d '{\n "foo": "bar",\n "baz": 1\n}'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
|
||||
test('Sends data with unresolved template tags', async () => {
|
||||
test("Sends data with unresolved template tags", async () => {
|
||||
expect(
|
||||
await convert(
|
||||
{
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
message: '{"timestamp": ${[ faker "timestamp" ]}, "foo": "bar"}',
|
||||
},
|
||||
['/foo.proto'],
|
||||
["/foo.proto"],
|
||||
),
|
||||
).toEqual(
|
||||
[
|
||||
`grpcurl -import-path '/'`,
|
||||
`-proto '/foo.proto'`,
|
||||
`-d '{"timestamp": \${[ faker "timestamp" ]}, "foo": "bar"}'`,
|
||||
'yaak.app',
|
||||
].join(' \\\n '),
|
||||
"yaak.app",
|
||||
].join(" \\\n "),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/action-send-folder",
|
||||
"displayName": "Send All",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Send all HTTP requests in a folder sequentially in tree order",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/action-send-folder"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
folderActions: [
|
||||
{
|
||||
label: 'Send All',
|
||||
icon: 'send_horizontal',
|
||||
label: "Send All",
|
||||
icon: "send_horizontal",
|
||||
async onSelect(ctx, args) {
|
||||
const targetFolder = args.folder;
|
||||
|
||||
@@ -17,8 +17,8 @@ export const plugin: PluginDefinition = {
|
||||
// Build the send order to match tree ordering:
|
||||
// sort siblings by sortPriority then updatedAt, and traverse folders depth-first.
|
||||
const compareByOrder = (
|
||||
a: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||
b: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||
a: Pick<(typeof allFolders)[number], "sortPriority" | "updatedAt">,
|
||||
b: Pick<(typeof allFolders)[number], "sortPriority" | "updatedAt">,
|
||||
) => {
|
||||
if (a.sortPriority === b.sortPriority) {
|
||||
return a.updatedAt > b.updatedAt ? 1 : -1;
|
||||
@@ -26,7 +26,10 @@ export const plugin: PluginDefinition = {
|
||||
return a.sortPriority - b.sortPriority;
|
||||
};
|
||||
|
||||
const childrenByFolderId = new Map<string, Array<typeof allFolders[number] | typeof allRequests[number]>>();
|
||||
const childrenByFolderId = new Map<
|
||||
string,
|
||||
Array<(typeof allFolders)[number] | (typeof allRequests)[number]>
|
||||
>();
|
||||
for (const folder of allFolders) {
|
||||
if (folder.folderId == null) continue;
|
||||
const children = childrenByFolderId.get(folder.folderId) ?? [];
|
||||
@@ -44,9 +47,9 @@ export const plugin: PluginDefinition = {
|
||||
const collectRequests = (folderId: string) => {
|
||||
const children = (childrenByFolderId.get(folderId) ?? []).slice().sort(compareByOrder);
|
||||
for (const child of children) {
|
||||
if (child.model === 'folder') {
|
||||
if (child.model === "folder") {
|
||||
collectRequests(child.id);
|
||||
} else if (child.model === 'http_request') {
|
||||
} else if (child.model === "http_request") {
|
||||
requestsToSend.push(child);
|
||||
}
|
||||
}
|
||||
@@ -55,9 +58,9 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
if (requestsToSend.length === 0) {
|
||||
await ctx.toast.show({
|
||||
message: 'No requests in folder',
|
||||
icon: 'info',
|
||||
color: 'info',
|
||||
message: "No requests in folder",
|
||||
icon: "info",
|
||||
color: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -79,15 +82,15 @@ export const plugin: PluginDefinition = {
|
||||
// Show summary toast
|
||||
if (errorCount === 0) {
|
||||
await ctx.toast.show({
|
||||
message: `Sent ${successCount} request${successCount !== 1 ? 's' : ''}`,
|
||||
icon: 'send_horizontal',
|
||||
color: 'success',
|
||||
message: `Sent ${successCount} request${successCount !== 1 ? "s" : ""}`,
|
||||
icon: "send_horizontal",
|
||||
color: "success",
|
||||
});
|
||||
} else {
|
||||
await ctx.toast.show({
|
||||
message: `Sent ${successCount}, failed ${errorCount}`,
|
||||
icon: 'alert_triangle',
|
||||
color: 'warning',
|
||||
icon: "alert_triangle",
|
||||
color: "warning",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-apikey",
|
||||
"displayName": "API Key Authentication",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using an API key",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-apikey"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'apikey',
|
||||
label: 'API Key',
|
||||
shortLabel: 'API Key',
|
||||
name: "apikey",
|
||||
label: "API Key",
|
||||
shortLabel: "API Key",
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'location',
|
||||
label: 'Behavior',
|
||||
defaultValue: 'header',
|
||||
type: "select",
|
||||
name: "location",
|
||||
label: "Behavior",
|
||||
defaultValue: "header",
|
||||
options: [
|
||||
{ label: 'Insert Header', value: 'header' },
|
||||
{ label: 'Append Query Parameter', value: 'query' },
|
||||
{ label: "Insert Header", value: "header" },
|
||||
{ label: "Append Query Parameter", value: "query" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'key',
|
||||
label: 'Key',
|
||||
type: "text",
|
||||
name: "key",
|
||||
label: "Key",
|
||||
dynamic: (_ctx, { values }) => {
|
||||
return values.location === 'query'
|
||||
return values.location === "query"
|
||||
? {
|
||||
label: 'Parameter Name',
|
||||
description: 'The name of the query parameter to add to the request',
|
||||
label: "Parameter Name",
|
||||
description: "The name of the query parameter to add to the request",
|
||||
}
|
||||
: {
|
||||
label: 'Header Name',
|
||||
description: 'The name of the header to add to the request',
|
||||
label: "Header Name",
|
||||
description: "The name of the header to add to the request",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
label: 'API Key',
|
||||
type: "text",
|
||||
name: "value",
|
||||
label: "API Key",
|
||||
optional: true,
|
||||
password: true,
|
||||
},
|
||||
],
|
||||
async onApply(_ctx, { values }) {
|
||||
const key = String(values.key ?? '');
|
||||
const value = String(values.value ?? '');
|
||||
const key = String(values.key ?? "");
|
||||
const value = String(values.value ?? "");
|
||||
const location = String(values.location);
|
||||
|
||||
if (location === 'query') {
|
||||
if (location === "query") {
|
||||
return { setQueryParameters: [{ name: key, value }] };
|
||||
}
|
||||
return { setHeaders: [{ name: key, value }] };
|
||||
|
||||
@@ -38,7 +38,7 @@ The plugin presents the following fields:
|
||||
|
||||
- **Access Key ID** – Your AWS access key identifier
|
||||
- **Secret Access Key** – The secret associated with the access key
|
||||
- **Session Token** *(optional)* – Used for temporary or assumed-role credentials (treated as secret)
|
||||
- **Session Token** _(optional)_ – Used for temporary or assumed-role credentials (treated as secret)
|
||||
- **Region** – AWS region (e.g., `us-east-1`)
|
||||
- **Service** – AWS service identifier (e.g., `sts`, `s3`, `execute-api`)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-aws",
|
||||
"displayName": "AWS SigV4",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using AWS SigV4 signing",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-aws"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { URL } from 'node:url';
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
|
||||
import type { Request } from 'aws4';
|
||||
import aws4 from 'aws4';
|
||||
import { URL } from "node:url";
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import type { CallHttpAuthenticationResponse } from "@yaakapp-internal/plugins";
|
||||
import type { Request } from "aws4";
|
||||
import aws4 from "aws4";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'awsv4',
|
||||
label: 'AWS Signature',
|
||||
shortLabel: 'AWS v4',
|
||||
name: "awsv4",
|
||||
label: "AWS Signature",
|
||||
shortLabel: "AWS v4",
|
||||
args: [
|
||||
{ name: 'accessKeyId', label: 'Access Key ID', type: 'text', password: true },
|
||||
{ name: "accessKeyId", label: "Access Key ID", type: "text", password: true },
|
||||
{
|
||||
name: 'secretAccessKey',
|
||||
label: 'Secret Access Key',
|
||||
type: 'text',
|
||||
name: "secretAccessKey",
|
||||
label: "Secret Access Key",
|
||||
type: "text",
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
name: 'service',
|
||||
label: 'Service Name',
|
||||
type: 'text',
|
||||
defaultValue: 'sts',
|
||||
placeholder: 'sts',
|
||||
description: 'The service that is receiving the request (sts, s3, sqs, ...)',
|
||||
name: "service",
|
||||
label: "Service Name",
|
||||
type: "text",
|
||||
defaultValue: "sts",
|
||||
placeholder: "sts",
|
||||
description: "The service that is receiving the request (sts, s3, sqs, ...)",
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
label: 'Region',
|
||||
type: 'text',
|
||||
placeholder: 'us-east-1',
|
||||
description: 'The region that is receiving the request (defaults to us-east-1)',
|
||||
name: "region",
|
||||
label: "Region",
|
||||
type: "text",
|
||||
placeholder: "us-east-1",
|
||||
description: "The region that is receiving the request (defaults to us-east-1)",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'sessionToken',
|
||||
label: 'Session Token',
|
||||
type: 'text',
|
||||
name: "sessionToken",
|
||||
label: "Session Token",
|
||||
type: "text",
|
||||
password: true,
|
||||
optional: true,
|
||||
description: 'Only required if you are using temporary credentials',
|
||||
description: "Only required if you are using temporary credentials",
|
||||
},
|
||||
],
|
||||
onApply(_ctx, { values, ...args }): CallHttpAuthenticationResponse {
|
||||
const accessKeyId = String(values.accessKeyId || '');
|
||||
const secretAccessKey = String(values.secretAccessKey || '');
|
||||
const sessionToken = String(values.sessionToken || '') || undefined;
|
||||
const accessKeyId = String(values.accessKeyId || "");
|
||||
const secretAccessKey = String(values.secretAccessKey || "");
|
||||
const sessionToken = String(values.sessionToken || "") || undefined;
|
||||
|
||||
const url = new URL(args.url);
|
||||
|
||||
const headers: NonNullable<Request['headers']> = {};
|
||||
for (const headerName of ['content-type', 'host', 'x-amz-date', 'x-amz-security-token']) {
|
||||
const headers: NonNullable<Request["headers"]> = {};
|
||||
for (const headerName of ["content-type", "host", "x-amz-date", "x-amz-security-token"]) {
|
||||
const v = args.headers.find((h) => h.name.toLowerCase() === headerName);
|
||||
if (v != null) {
|
||||
headers[headerName] = v.value;
|
||||
@@ -61,8 +61,8 @@ export const plugin: PluginDefinition = {
|
||||
{
|
||||
host: url.host,
|
||||
method: args.method,
|
||||
path: url.pathname + (url.search || ''),
|
||||
service: String(values.service || 'sts'),
|
||||
path: url.pathname + (url.search || ""),
|
||||
service: String(values.service || "sts"),
|
||||
region: values.region ? String(values.region) : undefined,
|
||||
headers,
|
||||
doNotEncodePath: true,
|
||||
@@ -80,8 +80,8 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
return {
|
||||
setHeaders: Object.entries(signature.headers)
|
||||
.filter(([name]) => name !== 'content-type') // Don't add this because we already have it
|
||||
.map(([name, value]) => ({ name, value: String(value || '') })),
|
||||
.filter(([name]) => name !== "content-type") // Don't add this because we already have it
|
||||
.map(([name, value]) => ({ name, value: String(value || "") })),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Basic Authentication
|
||||
# Basic Authentication
|
||||
|
||||
A simple Basic Authentication plugin that implements HTTP Basic Auth according
|
||||
to [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617), enabling secure
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-basic",
|
||||
"displayName": "Basic Authentication",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using Basic Auth",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-basic"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'basic',
|
||||
label: 'Basic Auth',
|
||||
shortLabel: 'Basic',
|
||||
name: "basic",
|
||||
label: "Basic Auth",
|
||||
shortLabel: "Basic",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
type: "text",
|
||||
name: "username",
|
||||
label: "Username",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: "text",
|
||||
name: "password",
|
||||
label: "Password",
|
||||
optional: true,
|
||||
password: true,
|
||||
},
|
||||
],
|
||||
async onApply(_ctx, { values }) {
|
||||
const username = values.username ?? '';
|
||||
const password = values.password ?? '';
|
||||
const value = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
return { setHeaders: [{ name: 'Authorization', value }] };
|
||||
const username = values.username ?? "";
|
||||
const password = values.password ?? "";
|
||||
const value = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
||||
return { setHeaders: [{ name: "Authorization", value }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,77 +1,87 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { plugin } from '../src';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { plugin } from "../src";
|
||||
|
||||
const ctx = {} as Context;
|
||||
|
||||
describe('auth-basic', () => {
|
||||
test('Both username and password', async () => {
|
||||
describe("auth-basic", () => {
|
||||
test("Both username and password", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { username: 'user', password: 'pass' },
|
||||
values: { username: "user", password: "pass" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({
|
||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('user:pass').toString('base64')}` }],
|
||||
setHeaders: [
|
||||
{ name: "Authorization", value: `Basic ${Buffer.from("user:pass").toString("base64")}` },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Empty password', async () => {
|
||||
test("Empty password", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { username: 'apikey', password: '' },
|
||||
values: { username: "apikey", password: "" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({
|
||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('apikey:').toString('base64')}` }],
|
||||
setHeaders: [
|
||||
{ name: "Authorization", value: `Basic ${Buffer.from("apikey:").toString("base64")}` },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Missing password (undefined)', async () => {
|
||||
test("Missing password (undefined)", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { username: 'apikey' },
|
||||
values: { username: "apikey" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({
|
||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('apikey:').toString('base64')}` }],
|
||||
setHeaders: [
|
||||
{ name: "Authorization", value: `Basic ${Buffer.from("apikey:").toString("base64")}` },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Missing username (undefined)', async () => {
|
||||
test("Missing username (undefined)", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { password: 'secret' },
|
||||
values: { password: "secret" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({
|
||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from(':secret').toString('base64')}` }],
|
||||
setHeaders: [
|
||||
{ name: "Authorization", value: `Basic ${Buffer.from(":secret").toString("base64")}` },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('No values (both undefined)', async () => {
|
||||
test("No values (both undefined)", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({
|
||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from(':').toString('base64')}` }],
|
||||
setHeaders: [
|
||||
{ name: "Authorization", value: `Basic ${Buffer.from(":").toString("base64")}` },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-bearer",
|
||||
"displayName": "Bearer Authentication",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using bearer authentication",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-bearer"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import type { CallHttpAuthenticationRequest } from "@yaakapp-internal/plugins";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'bearer',
|
||||
label: 'Bearer Token',
|
||||
shortLabel: 'Bearer',
|
||||
name: "bearer",
|
||||
label: "Bearer Token",
|
||||
shortLabel: "Bearer",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'token',
|
||||
label: 'Token',
|
||||
type: "text",
|
||||
name: "token",
|
||||
label: "Token",
|
||||
optional: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'prefix',
|
||||
label: 'Prefix',
|
||||
type: "text",
|
||||
name: "prefix",
|
||||
label: "Prefix",
|
||||
optional: true,
|
||||
placeholder: '',
|
||||
defaultValue: 'Bearer',
|
||||
placeholder: "",
|
||||
defaultValue: "Bearer",
|
||||
description:
|
||||
'The prefix to use for the Authorization header, which will be of the format "<PREFIX> <TOKEN>".',
|
||||
},
|
||||
@@ -31,9 +31,9 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
};
|
||||
|
||||
function generateAuthorizationHeader(values: CallHttpAuthenticationRequest['values']) {
|
||||
const token = String(values.token || '').trim();
|
||||
const prefix = String(values.prefix || '').trim();
|
||||
function generateAuthorizationHeader(values: CallHttpAuthenticationRequest["values"]) {
|
||||
const token = String(values.token || "").trim();
|
||||
const prefix = String(values.prefix || "").trim();
|
||||
const value = `${prefix} ${token}`.trim();
|
||||
return { name: 'Authorization', value };
|
||||
return { name: "Authorization", value };
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { plugin } from '../src';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { plugin } from "../src";
|
||||
|
||||
const ctx = {} as Context;
|
||||
|
||||
describe('auth-bearer', () => {
|
||||
test('No values', async () => {
|
||||
describe("auth-bearer", () => {
|
||||
test("No values", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({ setHeaders: [{ name: 'Authorization', value: '' }] });
|
||||
).toEqual({ setHeaders: [{ name: "Authorization", value: "" }] });
|
||||
});
|
||||
|
||||
test('Only token', async () => {
|
||||
test("Only token", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { token: 'my-token' },
|
||||
values: { token: "my-token" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'my-token' }] });
|
||||
).toEqual({ setHeaders: [{ name: "Authorization", value: "my-token" }] });
|
||||
});
|
||||
|
||||
test('Only prefix', async () => {
|
||||
test("Only prefix", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { prefix: 'Hello' },
|
||||
values: { prefix: "Hello" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello' }] });
|
||||
).toEqual({ setHeaders: [{ name: "Authorization", value: "Hello" }] });
|
||||
});
|
||||
|
||||
test('Prefix and token', async () => {
|
||||
test("Prefix and token", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { prefix: 'Hello', token: 'my-token' },
|
||||
values: { prefix: "Hello", token: "my-token" },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
|
||||
).toEqual({ setHeaders: [{ name: "Authorization", value: "Hello my-token" }] });
|
||||
});
|
||||
|
||||
test('Extra spaces', async () => {
|
||||
test("Extra spaces", async () => {
|
||||
expect(
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: { prefix: '\t Hello ', token: ' \nmy-token ' },
|
||||
values: { prefix: "\t Hello ", token: " \nmy-token " },
|
||||
headers: [],
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
contextId: '111',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
contextId: "111",
|
||||
}),
|
||||
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
|
||||
).toEqual({ setHeaders: [{ name: "Authorization", value: "Hello my-token" }] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-jwt",
|
||||
"displayName": "JSON Web Tokens",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using JSON web tokens (JWT)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-jwt"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,104 +1,104 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const algorithms = [
|
||||
'HS256',
|
||||
'HS384',
|
||||
'HS512',
|
||||
'RS256',
|
||||
'RS384',
|
||||
'RS512',
|
||||
'PS256',
|
||||
'PS384',
|
||||
'PS512',
|
||||
'ES256',
|
||||
'ES384',
|
||||
'ES512',
|
||||
'none',
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"none",
|
||||
] as const;
|
||||
|
||||
const defaultAlgorithm = algorithms[0];
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'jwt',
|
||||
label: 'JWT Bearer',
|
||||
shortLabel: 'JWT',
|
||||
name: "jwt",
|
||||
label: "JWT Bearer",
|
||||
shortLabel: "JWT",
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'algorithm',
|
||||
label: 'Algorithm',
|
||||
type: "select",
|
||||
name: "algorithm",
|
||||
label: "Algorithm",
|
||||
hideLabel: true,
|
||||
defaultValue: defaultAlgorithm,
|
||||
options: algorithms.map((value) => ({ label: value === 'none' ? 'None' : value, value })),
|
||||
options: algorithms.map((value) => ({ label: value === "none" ? "None" : value, value })),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'secret',
|
||||
label: 'Secret or Private Key',
|
||||
type: "text",
|
||||
name: "secret",
|
||||
label: "Secret or Private Key",
|
||||
password: true,
|
||||
optional: true,
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'secretBase64',
|
||||
label: 'Secret is base64 encoded',
|
||||
type: "checkbox",
|
||||
name: "secretBase64",
|
||||
label: "Secret is base64 encoded",
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'payload',
|
||||
label: 'JWT Payload',
|
||||
language: 'json',
|
||||
type: "editor",
|
||||
name: "payload",
|
||||
label: "JWT Payload",
|
||||
language: "json",
|
||||
defaultValue: '{\n "foo": "bar"\n}',
|
||||
placeholder: '{ }',
|
||||
placeholder: "{ }",
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'headers',
|
||||
label: 'JWT Header',
|
||||
description: 'Merged with auto-generated header fields like alg (e.g., kid)',
|
||||
language: 'json',
|
||||
defaultValue: '{}',
|
||||
placeholder: '{ }',
|
||||
type: "editor",
|
||||
name: "headers",
|
||||
label: "JWT Header",
|
||||
description: "Merged with auto-generated header fields like alg (e.g., kid)",
|
||||
language: "json",
|
||||
defaultValue: "{}",
|
||||
placeholder: "{ }",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'location',
|
||||
label: 'Behavior',
|
||||
defaultValue: 'header',
|
||||
type: "select",
|
||||
name: "location",
|
||||
label: "Behavior",
|
||||
defaultValue: "header",
|
||||
options: [
|
||||
{ label: 'Insert Header', value: 'header' },
|
||||
{ label: 'Append Query Parameter', value: 'query' },
|
||||
{ label: "Insert Header", value: "header" },
|
||||
{ label: "Append Query Parameter", value: "query" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Header Name',
|
||||
defaultValue: 'Authorization',
|
||||
type: "text",
|
||||
name: "name",
|
||||
label: "Header Name",
|
||||
defaultValue: "Authorization",
|
||||
optional: true,
|
||||
description: 'The name of the header to add to the request',
|
||||
description: "The name of the header to add to the request",
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerPrefix',
|
||||
label: 'Header Prefix',
|
||||
type: "text",
|
||||
name: "headerPrefix",
|
||||
label: "Header Prefix",
|
||||
optional: true,
|
||||
defaultValue: 'Bearer',
|
||||
defaultValue: "Bearer",
|
||||
},
|
||||
],
|
||||
dynamic(_ctx, args) {
|
||||
if (args.values.location === 'query') {
|
||||
if (args.values.location === "query") {
|
||||
return {
|
||||
hidden: true,
|
||||
};
|
||||
@@ -106,14 +106,14 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Parameter Name',
|
||||
description: 'The name of the query parameter to add to the request',
|
||||
defaultValue: 'token',
|
||||
type: "text",
|
||||
name: "name",
|
||||
label: "Parameter Name",
|
||||
description: "The name of the query parameter to add to the request",
|
||||
defaultValue: "token",
|
||||
optional: true,
|
||||
dynamic(_ctx, args) {
|
||||
if (args.values.location !== 'query') {
|
||||
if (args.values.location !== "query") {
|
||||
return {
|
||||
hidden: true,
|
||||
};
|
||||
@@ -125,7 +125,7 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
async onApply(_ctx, { values }) {
|
||||
const { algorithm, secret: _secret, secretBase64, payload, headers } = values;
|
||||
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
|
||||
const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`;
|
||||
|
||||
const parsedHeaders = headers ? JSON.parse(`${headers}`) : undefined;
|
||||
|
||||
@@ -135,12 +135,12 @@ export const plugin: PluginDefinition = {
|
||||
header: parsedHeaders as jwt.JwtHeader | undefined,
|
||||
});
|
||||
|
||||
if (values.location === 'query') {
|
||||
const paramName = String(values.name || 'token');
|
||||
if (values.location === "query") {
|
||||
const paramName = String(values.name || "token");
|
||||
return { setQueryParameters: [{ name: paramName, value: token }] };
|
||||
}
|
||||
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
|
||||
const headerName = String(values.name || 'Authorization');
|
||||
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : "Bearer";
|
||||
const headerName = String(values.name || "Authorization");
|
||||
const headerValue = `${headerPrefix} ${token}`.trim();
|
||||
return { setHeaders: [{ name: headerName, value: headerValue }] };
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
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(','))
|
||||
.filter((h) => h.name.toLowerCase() === "www-authenticate")
|
||||
.flatMap((h) => h.value.split(","))
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -14,40 +14,40 @@ function extractNtlmChallenge(headers: Array<{ name: string; value: string }>):
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'windows',
|
||||
label: 'NTLM Auth',
|
||||
shortLabel: 'NTLM',
|
||||
name: "windows",
|
||||
label: "NTLM Auth",
|
||||
shortLabel: "NTLM",
|
||||
args: [
|
||||
{
|
||||
type: 'banner',
|
||||
color: 'info',
|
||||
type: "banner",
|
||||
color: "info",
|
||||
inputs: [
|
||||
{
|
||||
type: 'markdown',
|
||||
type: "markdown",
|
||||
content:
|
||||
'NTLM is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).',
|
||||
"NTLM is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
type: "text",
|
||||
name: "username",
|
||||
label: "Username",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: "text",
|
||||
name: "password",
|
||||
label: "Password",
|
||||
optional: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{ name: 'domain', label: 'Domain', type: 'text', optional: true },
|
||||
{ name: 'workstation', label: 'Workstation', type: 'text', optional: true },
|
||||
{ name: "domain", label: "Domain", type: "text", optional: true },
|
||||
{ name: "workstation", label: "Workstation", type: "text", optional: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -72,15 +72,15 @@ export const plugin: PluginDefinition = {
|
||||
method,
|
||||
url,
|
||||
headers: [
|
||||
{ name: 'Authorization', value: type1 },
|
||||
{ name: 'Connection', value: 'keep-alive' },
|
||||
{ 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');
|
||||
throw new Error("Unable to find NTLM challenge in WWW-Authenticate response headers");
|
||||
}
|
||||
|
||||
const type2 = ntlm.parseType2Message(ntlmChallenge, (err: Error | null) => {
|
||||
@@ -88,7 +88,7 @@ export const plugin: PluginDefinition = {
|
||||
});
|
||||
const type3 = ntlm.createType3Message(type2, options);
|
||||
|
||||
return { setHeaders: [{ name: 'Authorization', value: type3 }] };
|
||||
return { setHeaders: [{ name: "Authorization", value: type3 }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
2
plugins/auth-ntlm/src/modules.d.ts
vendored
2
plugins/auth-ntlm/src/modules.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'httpntlm';
|
||||
declare module "httpntlm";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vite-plus/test';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { beforeEach, describe, expect, test, vi } from "vite-plus/test";
|
||||
|
||||
const ntlmMock = vi.hoisted(() => ({
|
||||
createType1Message: vi.fn(),
|
||||
@@ -7,26 +7,26 @@ const ntlmMock = vi.hoisted(() => ({
|
||||
createType3Message: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('httpntlm', () => ({ ntlm: ntlmMock }));
|
||||
vi.mock("httpntlm", () => ({ ntlm: ntlmMock }));
|
||||
|
||||
import { plugin } from '../src';
|
||||
import { plugin } from "../src";
|
||||
|
||||
describe('auth-ntlm', () => {
|
||||
describe("auth-ntlm", () => {
|
||||
beforeEach(() => {
|
||||
ntlmMock.createType1Message.mockReset();
|
||||
ntlmMock.parseType2Message.mockReset();
|
||||
ntlmMock.createType3Message.mockReset();
|
||||
ntlmMock.createType1Message.mockReturnValue('NTLM TYPE1');
|
||||
ntlmMock.createType1Message.mockReturnValue("NTLM TYPE1");
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ntlmMock.parseType2Message.mockReturnValue({} as any);
|
||||
ntlmMock.createType3Message.mockReturnValue('NTLM TYPE3');
|
||||
ntlmMock.createType3Message.mockReturnValue("NTLM TYPE3");
|
||||
});
|
||||
|
||||
test('uses NTLM challenge when Negotiate and NTLM headers are separate', async () => {
|
||||
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==' },
|
||||
{ name: "WWW-Authenticate", value: "Negotiate" },
|
||||
{ name: "WWW-Authenticate", value: "NTLM TlRMTVNTUAACAAAAAA==" },
|
||||
],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
@@ -34,41 +34,41 @@ describe('auth-ntlm', () => {
|
||||
const result = await plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://example.local/resource',
|
||||
method: 'GET',
|
||||
contextId: 'ctx',
|
||||
url: "https://example.local/resource",
|
||||
method: "GET",
|
||||
contextId: "ctx",
|
||||
});
|
||||
|
||||
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
|
||||
'NTLM TlRMTVNTUAACAAAAAA==',
|
||||
"NTLM TlRMTVNTUAACAAAAAA==",
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(result).toEqual({ setHeaders: [{ name: 'Authorization', value: 'NTLM TYPE3' }] });
|
||||
expect(result).toEqual({ setHeaders: [{ name: "Authorization", value: "NTLM TYPE3" }] });
|
||||
});
|
||||
|
||||
test('uses NTLM challenge when auth schemes are comma-separated in one header', async () => {
|
||||
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==' }],
|
||||
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',
|
||||
url: "https://example.local/resource",
|
||||
method: "GET",
|
||||
contextId: "ctx",
|
||||
});
|
||||
|
||||
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
|
||||
'NTLM TlRMTVNTUAACAAAAAA==',
|
||||
"NTLM TlRMTVNTUAACAAAAAA==",
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws a clear error when NTLM challenge is missing', async () => {
|
||||
test("throws a clear error when NTLM challenge is missing", async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
headers: [{ name: 'WWW-Authenticate', value: 'Negotiate' }],
|
||||
headers: [{ name: "WWW-Authenticate", value: "Negotiate" }],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
@@ -76,10 +76,10 @@ describe('auth-ntlm', () => {
|
||||
plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://example.local/resource',
|
||||
method: 'GET',
|
||||
contextId: 'ctx',
|
||||
url: "https://example.local/resource",
|
||||
method: "GET",
|
||||
contextId: "ctx",
|
||||
}),
|
||||
).rejects.toThrow('Unable to find NTLM challenge in WWW-Authenticate response headers');
|
||||
).rejects.toThrow("Unable to find NTLM challenge in WWW-Authenticate response headers");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-oauth1",
|
||||
"displayName": "OAuth 1.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using OAuth 1.0a",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-oauth1"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import crypto from 'node:crypto';
|
||||
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from '@yaakapp/api';
|
||||
import OAuth from 'oauth-1.0a';
|
||||
import crypto from "node:crypto";
|
||||
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from "@yaakapp/api";
|
||||
import OAuth from "oauth-1.0a";
|
||||
|
||||
const signatures = {
|
||||
HMAC_SHA1: 'HMAC-SHA1',
|
||||
HMAC_SHA256: 'HMAC-SHA256',
|
||||
HMAC_SHA512: 'HMAC-SHA512',
|
||||
RSA_SHA1: 'RSA-SHA1',
|
||||
RSA_SHA256: 'RSA-SHA256',
|
||||
RSA_SHA512: 'RSA-SHA512',
|
||||
PLAINTEXT: 'PLAINTEXT',
|
||||
HMAC_SHA1: "HMAC-SHA1",
|
||||
HMAC_SHA256: "HMAC-SHA256",
|
||||
HMAC_SHA512: "HMAC-SHA512",
|
||||
RSA_SHA1: "RSA-SHA1",
|
||||
RSA_SHA256: "RSA-SHA256",
|
||||
RSA_SHA512: "RSA-SHA512",
|
||||
PLAINTEXT: "PLAINTEXT",
|
||||
} as const;
|
||||
const defaultSig = signatures.HMAC_SHA1;
|
||||
|
||||
const pkSigs = Object.values(signatures).filter((k) => k.startsWith('RSA-'));
|
||||
const pkSigs = Object.values(signatures).filter((k) => k.startsWith("RSA-"));
|
||||
const nonPkSigs = Object.values(signatures).filter((k) => !pkSigs.includes(k));
|
||||
|
||||
type SigMethod = (typeof signatures)[keyof typeof signatures];
|
||||
|
||||
function hiddenIfNot(
|
||||
sigMethod: SigMethod[],
|
||||
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
|
||||
...other: ((values: GetHttpAuthenticationConfigRequest["values"]) => boolean)[]
|
||||
) {
|
||||
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
|
||||
const hasGrantType = sigMethod.find((t) => t === String(values.signatureMethod ?? defaultSig));
|
||||
@@ -32,78 +32,78 @@ function hiddenIfNot(
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'oauth1',
|
||||
label: 'OAuth 1.0',
|
||||
shortLabel: 'OAuth 1',
|
||||
name: "oauth1",
|
||||
label: "OAuth 1.0",
|
||||
shortLabel: "OAuth 1",
|
||||
args: [
|
||||
{
|
||||
type: 'banner',
|
||||
color: 'info',
|
||||
type: "banner",
|
||||
color: "info",
|
||||
inputs: [
|
||||
{
|
||||
type: 'markdown',
|
||||
type: "markdown",
|
||||
content:
|
||||
'OAuth 1.0 is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).',
|
||||
"OAuth 1.0 is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'signatureMethod',
|
||||
label: 'Signature Method',
|
||||
type: 'select',
|
||||
name: "signatureMethod",
|
||||
label: "Signature Method",
|
||||
type: "select",
|
||||
defaultValue: defaultSig,
|
||||
options: Object.values(signatures).map((v) => ({ label: v, value: v })),
|
||||
},
|
||||
{ name: 'consumerKey', label: 'Consumer Key', type: 'text', password: true, optional: true },
|
||||
{ name: "consumerKey", label: "Consumer Key", type: "text", password: true, optional: true },
|
||||
{
|
||||
name: 'consumerSecret',
|
||||
label: 'Consumer Secret',
|
||||
type: 'text',
|
||||
name: "consumerSecret",
|
||||
label: "Consumer Secret",
|
||||
type: "text",
|
||||
password: true,
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'tokenKey',
|
||||
label: 'Access Token',
|
||||
type: 'text',
|
||||
name: "tokenKey",
|
||||
label: "Access Token",
|
||||
type: "text",
|
||||
password: true,
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'tokenSecret',
|
||||
label: 'Token Secret',
|
||||
type: 'text',
|
||||
name: "tokenSecret",
|
||||
label: "Token Secret",
|
||||
type: "text",
|
||||
password: true,
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(nonPkSigs),
|
||||
},
|
||||
{
|
||||
name: 'privateKey',
|
||||
label: 'Private Key (RSA-SHA1)',
|
||||
type: 'text',
|
||||
name: "privateKey",
|
||||
label: "Private Key (RSA-SHA1)",
|
||||
type: "text",
|
||||
multiLine: true,
|
||||
optional: true,
|
||||
password: true,
|
||||
placeholder:
|
||||
'-----BEGIN RSA PRIVATE KEY-----\nPrivate key in PEM format\n-----END RSA PRIVATE KEY-----',
|
||||
"-----BEGIN RSA PRIVATE KEY-----\nPrivate key in PEM format\n-----END RSA PRIVATE KEY-----",
|
||||
dynamic: hiddenIfNot(pkSigs),
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{ name: 'callback', label: 'Callback Url', type: 'text', optional: true },
|
||||
{ name: 'verifier', label: 'Verifier', type: 'text', optional: true, password: true },
|
||||
{ name: 'timestamp', label: 'Timestamp', type: 'text', optional: true },
|
||||
{ name: 'nonce', label: 'Nonce', type: 'text', optional: true },
|
||||
{ name: "callback", label: "Callback Url", type: "text", optional: true },
|
||||
{ name: "verifier", label: "Verifier", type: "text", optional: true, password: true },
|
||||
{ name: "timestamp", label: "Timestamp", type: "text", optional: true },
|
||||
{ name: "nonce", label: "Nonce", type: "text", optional: true },
|
||||
{
|
||||
name: 'version',
|
||||
label: 'OAuth Version',
|
||||
type: 'text',
|
||||
name: "version",
|
||||
label: "OAuth Version",
|
||||
type: "text",
|
||||
optional: true,
|
||||
defaultValue: '1.0',
|
||||
defaultValue: "1.0",
|
||||
},
|
||||
{ name: 'realm', label: 'Realm', type: 'text', optional: true },
|
||||
{ name: "realm", label: "Realm", type: "text", optional: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -115,12 +115,12 @@ export const plugin: PluginDefinition = {
|
||||
setHeaders?: { name: string; value: string }[];
|
||||
setQueryParameters?: { name: string; value: string }[];
|
||||
} {
|
||||
const consumerKey = String(values.consumerKey || '');
|
||||
const consumerSecret = String(values.consumerSecret || '');
|
||||
const consumerKey = String(values.consumerKey || "");
|
||||
const consumerSecret = String(values.consumerSecret || "");
|
||||
|
||||
const signatureMethod = String(values.signatureMethod || signatures.HMAC_SHA1) as SigMethod;
|
||||
const version = String(values.version || '1.0');
|
||||
const realm = String(values.realm || '') || undefined;
|
||||
const version = String(values.version || "1.0");
|
||||
const realm = String(values.realm || "") || undefined;
|
||||
|
||||
const oauth = new OAuth({
|
||||
consumer: { key: consumerKey, secret: consumerSecret },
|
||||
@@ -131,13 +131,13 @@ export const plugin: PluginDefinition = {
|
||||
});
|
||||
|
||||
if (pkSigs.includes(signatureMethod)) {
|
||||
oauth.getSigningKey = (tokenSecret?: string) => tokenSecret || '';
|
||||
oauth.getSigningKey = (tokenSecret?: string) => tokenSecret || "";
|
||||
}
|
||||
|
||||
const requestUrl = new URL(url);
|
||||
|
||||
// Base request options passed to oauth-1.0a
|
||||
const requestData: Omit<OAuth.RequestOptions, 'data'> & {
|
||||
const requestData: Omit<OAuth.RequestOptions, "data"> & {
|
||||
data: Record<string, string | string[]>;
|
||||
} = {
|
||||
method,
|
||||
@@ -148,7 +148,7 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
// (1) Include existing query params in signature base string
|
||||
for (const key of requestUrl.searchParams.keys()) {
|
||||
if (key.startsWith('oauth_')) continue;
|
||||
if (key.startsWith("oauth_")) continue;
|
||||
const all = requestUrl.searchParams.getAll(key);
|
||||
const first = all[0];
|
||||
if (first == null) continue;
|
||||
@@ -165,8 +165,8 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
if (pkSigs.includes(signatureMethod)) {
|
||||
token = {
|
||||
key: String(values.tokenKey || ''),
|
||||
secret: String(values.privateKey || ''),
|
||||
key: String(values.tokenKey || ""),
|
||||
secret: String(values.privateKey || ""),
|
||||
};
|
||||
} else if (values.tokenKey && values.tokenSecret) {
|
||||
token = { key: String(values.tokenKey), secret: String(values.tokenSecret) };
|
||||
@@ -176,7 +176,7 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
const authParams = oauth.authorize(requestData, token as OAuth.Token | undefined);
|
||||
const { Authorization } = oauth.toHeader(authParams);
|
||||
return { setHeaders: [{ name: 'Authorization', value: Authorization }] };
|
||||
return { setHeaders: [{ name: "Authorization", value: Authorization }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -185,26 +185,26 @@ function hashFunction(signatureMethod: SigMethod) {
|
||||
switch (signatureMethod) {
|
||||
case signatures.HMAC_SHA1:
|
||||
return (base: string, key: string) =>
|
||||
crypto.createHmac('sha1', key).update(base).digest('base64');
|
||||
crypto.createHmac("sha1", key).update(base).digest("base64");
|
||||
case signatures.HMAC_SHA256:
|
||||
return (base: string, key: string) =>
|
||||
crypto.createHmac('sha256', key).update(base).digest('base64');
|
||||
crypto.createHmac("sha256", key).update(base).digest("base64");
|
||||
case signatures.HMAC_SHA512:
|
||||
return (base: string, key: string) =>
|
||||
crypto.createHmac('sha512', key).update(base).digest('base64');
|
||||
crypto.createHmac("sha512", key).update(base).digest("base64");
|
||||
case signatures.RSA_SHA1:
|
||||
return (base: string, privateKey: string) =>
|
||||
crypto.createSign('RSA-SHA1').update(base).sign(privateKey, 'base64');
|
||||
crypto.createSign("RSA-SHA1").update(base).sign(privateKey, "base64");
|
||||
case signatures.RSA_SHA256:
|
||||
return (base: string, privateKey: string) =>
|
||||
crypto.createSign('RSA-SHA256').update(base).sign(privateKey, 'base64');
|
||||
crypto.createSign("RSA-SHA256").update(base).sign(privateKey, "base64");
|
||||
case signatures.RSA_SHA512:
|
||||
return (base: string, privateKey: string) =>
|
||||
crypto.createSign('RSA-SHA512').update(base).sign(privateKey, 'base64');
|
||||
crypto.createSign("RSA-SHA512").update(base).sign(privateKey, "base64");
|
||||
case signatures.PLAINTEXT:
|
||||
return (base: string) => base;
|
||||
default:
|
||||
return (base: string, key: string) =>
|
||||
crypto.createHmac('sha1', key).update(base).digest('base64');
|
||||
crypto.createHmac("sha1", key).update(base).digest("base64");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ The most secure and commonly used OAuth 2.0 flow for web applications.
|
||||
|
||||
### Client Credentials Flow
|
||||
|
||||
Ideal for server-to-server authentication where no user interaction is required.
|
||||
Ideal for server-to-server authentication where no user interaction is required.
|
||||
|
||||
### Implicit Flow
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/auth-oauth2",
|
||||
"displayName": "OAuth 2.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Authenticate requests using OAuth 2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/auth-oauth2"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import http from 'node:http';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import http from "node:http";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
|
||||
export const HOSTED_CALLBACK_URL_BASE = 'https://oauth.yaak.app/redirect';
|
||||
export const HOSTED_CALLBACK_URL_BASE = "https://oauth.yaak.app/redirect";
|
||||
export const DEFAULT_LOCALHOST_PORT = 8765;
|
||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
@@ -36,12 +36,12 @@ export function startCallbackServer(options: {
|
||||
}): Promise<CallbackServerResult> {
|
||||
// Stop any previously active server before starting a new one
|
||||
if (activeServer) {
|
||||
console.log('[oauth2] Stopping previous callback server before starting new one');
|
||||
console.log("[oauth2] Stopping previous callback server before starting new one");
|
||||
activeServer.stop();
|
||||
activeServer = null;
|
||||
}
|
||||
|
||||
const { port = 0, path = '/callback', timeoutMs = CALLBACK_TIMEOUT_MS } = options;
|
||||
const { port = 0, path = "/callback", timeoutMs = CALLBACK_TIMEOUT_MS } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let callbackResolve: ((url: string) => void) | null = null;
|
||||
@@ -50,33 +50,33 @@ export function startCallbackServer(options: {
|
||||
let stopped = false;
|
||||
|
||||
const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
const reqUrl = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
||||
const reqUrl = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
||||
|
||||
// Only handle the callback path
|
||||
if (reqUrl.pathname !== path && reqUrl.pathname !== `${path}/`) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (req.method === "POST") {
|
||||
// POST: read JSON body with the final callback URL and resolve
|
||||
let body = '';
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
let body = "";
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on('end', () => {
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const { url: callbackUrl } = JSON.parse(body);
|
||||
if (!callbackUrl || typeof callbackUrl !== 'string') {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
res.end('Missing url in request body');
|
||||
if (!callbackUrl || typeof callbackUrl !== "string") {
|
||||
res.writeHead(400, { "Content-Type": "text/plain" });
|
||||
res.end("Missing url in request body");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send success response
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('OK');
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.end("OK");
|
||||
|
||||
// Resolve the callback promise
|
||||
if (callbackResolve) {
|
||||
@@ -88,19 +88,19 @@ export function startCallbackServer(options: {
|
||||
// Stop the server after a short delay to ensure response is sent
|
||||
setTimeout(() => stopServer(), 100);
|
||||
} catch {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
res.end('Invalid JSON');
|
||||
res.writeHead(400, { "Content-Type": "text/plain" });
|
||||
res.end("Invalid JSON");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// GET: serve intermediate page that reads the fragment and POSTs back
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.writeHead(200, { "Content-Type": "text/html" });
|
||||
res.end(getFragmentForwardingHtml());
|
||||
});
|
||||
|
||||
server.on('error', (err: Error) => {
|
||||
server.on("error", (err: Error) => {
|
||||
if (!stopped) {
|
||||
reject(err);
|
||||
}
|
||||
@@ -123,16 +123,16 @@ export function startCallbackServer(options: {
|
||||
server.close();
|
||||
|
||||
if (callbackReject) {
|
||||
callbackReject(new Error('Callback server stopped'));
|
||||
callbackReject(new Error("Callback server stopped"));
|
||||
callbackResolve = null;
|
||||
callbackReject = null;
|
||||
}
|
||||
};
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
server.listen(port, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('Failed to get server address'));
|
||||
if (!address || typeof address === "string") {
|
||||
reject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function startCallbackServer(options: {
|
||||
waitForCallback: () => {
|
||||
return new Promise<string>((res, rej) => {
|
||||
if (stopped) {
|
||||
rej(new Error('Callback server already stopped'));
|
||||
rej(new Error("Callback server already stopped"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function startCallbackServer(options: {
|
||||
// Set timeout
|
||||
timeoutHandle = setTimeout(() => {
|
||||
if (callbackReject) {
|
||||
callbackReject(new Error('Authorization timed out'));
|
||||
callbackReject(new Error("Authorization timed out"));
|
||||
callbackResolve = null;
|
||||
callbackReject = null;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function buildHostedCallbackRedirectUri(localPort: number): string {
|
||||
*/
|
||||
export function stopActiveServer(): void {
|
||||
if (activeServer) {
|
||||
console.log('[oauth2] Stopping active callback server during dispose');
|
||||
console.log("[oauth2] Stopping active callback server during dispose");
|
||||
activeServer.stop();
|
||||
activeServer = null;
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export async function getRedirectUrlViaExternalBrowser(
|
||||
ctx: Context,
|
||||
authorizationUrl: URL,
|
||||
options: {
|
||||
callbackType: 'localhost' | 'hosted';
|
||||
callbackType: "localhost" | "hosted";
|
||||
callbackPort?: number;
|
||||
},
|
||||
): Promise<{ callbackUrl: string; redirectUri: string }> {
|
||||
@@ -222,31 +222,31 @@ export async function getRedirectUrlViaExternalBrowser(
|
||||
|
||||
const server = await startCallbackServer({
|
||||
port,
|
||||
path: '/callback',
|
||||
path: "/callback",
|
||||
});
|
||||
|
||||
try {
|
||||
// Determine the redirect URI to send to the OAuth provider
|
||||
let oauthRedirectUri: string;
|
||||
|
||||
if (callbackType === 'hosted') {
|
||||
if (callbackType === "hosted") {
|
||||
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port);
|
||||
console.log('[oauth2] Using hosted callback redirect:', oauthRedirectUri);
|
||||
console.log("[oauth2] Using hosted callback redirect:", oauthRedirectUri);
|
||||
} else {
|
||||
oauthRedirectUri = server.redirectUri;
|
||||
console.log('[oauth2] Using localhost callback redirect:', oauthRedirectUri);
|
||||
console.log("[oauth2] Using localhost callback redirect:", oauthRedirectUri);
|
||||
}
|
||||
|
||||
// Set the redirect URI on the authorization URL
|
||||
authorizationUrl.searchParams.set('redirect_uri', oauthRedirectUri);
|
||||
authorizationUrl.searchParams.set("redirect_uri", oauthRedirectUri);
|
||||
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log('[oauth2] Opening external browser:', authorizationUrlStr);
|
||||
console.log("[oauth2] Opening external browser:", authorizationUrlStr);
|
||||
|
||||
// Show toast to inform user
|
||||
await ctx.toast.show({
|
||||
message: 'Opening browser for authorization...',
|
||||
icon: 'info',
|
||||
message: "Opening browser for authorization...",
|
||||
icon: "info",
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
@@ -254,10 +254,10 @@ export async function getRedirectUrlViaExternalBrowser(
|
||||
await ctx.window.openExternalUrl(authorizationUrlStr);
|
||||
|
||||
// Wait for the callback
|
||||
console.log('[oauth2] Waiting for callback on', server.redirectUri);
|
||||
console.log("[oauth2] Waiting for callback on", server.redirectUri);
|
||||
const callbackUrl = await server.waitForCallback();
|
||||
|
||||
console.log('[oauth2] Received callback:', callbackUrl);
|
||||
console.log("[oauth2] Received callback:", callbackUrl);
|
||||
|
||||
return { callbackUrl, redirectUri: oauthRedirectUri };
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
|
||||
import type { AccessTokenRawResponse } from './store';
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { Context, HttpRequest, HttpUrlParameter } from "@yaakapp/api";
|
||||
import type { AccessTokenRawResponse } from "./store";
|
||||
|
||||
export async function fetchAccessToken(
|
||||
ctx: Context,
|
||||
@@ -14,58 +14,58 @@ export async function fetchAccessToken(
|
||||
} & ({ clientAssertion: string } | { clientSecret: string; credentialsInBody: boolean }),
|
||||
): Promise<AccessTokenRawResponse> {
|
||||
const { clientId, grantType, accessTokenUrl, scope, audience, params } = args;
|
||||
console.log('[oauth2] Getting access token', accessTokenUrl);
|
||||
console.log("[oauth2] Getting access token", accessTokenUrl);
|
||||
const httpRequest: Partial<HttpRequest> = {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
url: accessTokenUrl,
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'grant_type', value: grantType }, ...params],
|
||||
form: [{ name: "grant_type", value: grantType }, ...params],
|
||||
},
|
||||
headers: [
|
||||
{ name: 'User-Agent', value: 'yaak' },
|
||||
{ name: "User-Agent", value: "yaak" },
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
name: "Accept",
|
||||
value: "application/x-www-form-urlencoded, application/json",
|
||||
},
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
{ name: "Content-Type", value: "application/x-www-form-urlencoded" },
|
||||
],
|
||||
};
|
||||
|
||||
if (scope) httpRequest.body?.form.push({ name: 'scope', value: scope });
|
||||
if (audience) httpRequest.body?.form.push({ name: 'audience', value: audience });
|
||||
if (scope) httpRequest.body?.form.push({ name: "scope", value: scope });
|
||||
if (audience) httpRequest.body?.form.push({ name: "audience", value: audience });
|
||||
|
||||
if ('clientAssertion' in args) {
|
||||
httpRequest.body?.form.push({ name: 'client_id', value: clientId });
|
||||
if ("clientAssertion" in args) {
|
||||
httpRequest.body?.form.push({ name: "client_id", value: clientId });
|
||||
httpRequest.body?.form.push({
|
||||
name: 'client_assertion_type',
|
||||
value: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
||||
name: "client_assertion_type",
|
||||
value: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
});
|
||||
httpRequest.body?.form.push({
|
||||
name: 'client_assertion',
|
||||
name: "client_assertion",
|
||||
value: args.clientAssertion,
|
||||
});
|
||||
} else if (args.credentialsInBody) {
|
||||
httpRequest.body?.form.push({ name: 'client_id', value: clientId });
|
||||
httpRequest.body?.form.push({ name: "client_id", value: clientId });
|
||||
httpRequest.body?.form.push({
|
||||
name: 'client_secret',
|
||||
name: "client_secret",
|
||||
value: args.clientSecret,
|
||||
});
|
||||
} else {
|
||||
const value = `Basic ${Buffer.from(`${clientId}:${args.clientSecret}`).toString('base64')}`;
|
||||
httpRequest.headers?.push({ name: 'Authorization', value });
|
||||
const value = `Basic ${Buffer.from(`${clientId}:${args.clientSecret}`).toString("base64")}`;
|
||||
httpRequest.headers?.push({ name: "Authorization", value });
|
||||
}
|
||||
|
||||
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
|
||||
httpRequest.authenticationType = "none"; // Don't inherit workspace auth
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
|
||||
console.log('[oauth2] Got access token response', resp.status);
|
||||
console.log("[oauth2] Got access token response", resp.status);
|
||||
|
||||
if (resp.error) {
|
||||
throw new Error(`Failed to fetch access token: ${resp.error}`);
|
||||
}
|
||||
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, "utf8") : "";
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(`Failed to fetch access token with status=${resp.status} and body=${body}`);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import type { Context, HttpRequest } from '@yaakapp/api';
|
||||
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
|
||||
import { deleteToken, getToken, storeToken } from './store';
|
||||
import { isTokenExpired } from './util';
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { Context, HttpRequest } from "@yaakapp/api";
|
||||
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from "./store";
|
||||
import { deleteToken, getToken, storeToken } from "./store";
|
||||
import { isTokenExpired } from "./util";
|
||||
|
||||
export async function getOrRefreshAccessToken(
|
||||
ctx: Context,
|
||||
@@ -42,33 +42,33 @@ export async function getOrRefreshAccessToken(
|
||||
|
||||
// Access token is expired, so get a new one
|
||||
const httpRequest: Partial<HttpRequest> = {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
url: accessTokenUrl,
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'grant_type', value: 'refresh_token' },
|
||||
{ name: 'refresh_token', value: token.response.refresh_token },
|
||||
{ name: "grant_type", value: "refresh_token" },
|
||||
{ name: "refresh_token", value: token.response.refresh_token },
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{ name: 'User-Agent', value: 'yaak' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
{ name: "User-Agent", value: "yaak" },
|
||||
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
|
||||
{ name: "Content-Type", value: "application/x-www-form-urlencoded" },
|
||||
],
|
||||
};
|
||||
|
||||
if (scope) httpRequest.body?.form.push({ name: 'scope', value: scope });
|
||||
if (scope) httpRequest.body?.form.push({ name: "scope", value: scope });
|
||||
|
||||
if (credentialsInBody) {
|
||||
httpRequest.body?.form.push({ name: 'client_id', value: clientId });
|
||||
httpRequest.body?.form.push({ name: 'client_secret', value: clientSecret });
|
||||
httpRequest.body?.form.push({ name: "client_id", value: clientId });
|
||||
httpRequest.body?.form.push({ name: "client_secret", value: clientSecret });
|
||||
} else {
|
||||
const value = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
httpRequest.headers?.push({ name: 'Authorization', value });
|
||||
const value = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
||||
httpRequest.headers?.push({ name: "Authorization", value });
|
||||
}
|
||||
|
||||
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
|
||||
httpRequest.authenticationType = "none"; // Don't inherit workspace auth
|
||||
const resp = await ctx.httpRequest.send({ httpRequest });
|
||||
|
||||
if (resp.error) {
|
||||
@@ -78,14 +78,14 @@ export async function getOrRefreshAccessToken(
|
||||
if (resp.status >= 400 && resp.status < 500) {
|
||||
// Client errors (4xx) indicate the refresh token is invalid, expired, or revoked
|
||||
// Delete the token and return null to trigger a fresh authorization flow
|
||||
console.log('[oauth2] Refresh token request failed with client error, deleting token');
|
||||
console.log("[oauth2] Refresh token request failed with client error, deleting token");
|
||||
await deleteToken(ctx, tokenArgs);
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
|
||||
const body = resp.bodyPath ? readFileSync(resp.bodyPath, "utf8") : "";
|
||||
|
||||
console.log('[oauth2] Got refresh token response', resp.status);
|
||||
console.log("[oauth2] Got refresh token response", resp.status);
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(`Failed to refresh access token with status=${resp.status} and body=${body}`);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { getRedirectUrlViaExternalBrowser } from '../callbackServer';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import type { AccessToken, TokenStoreArgs } from '../store';
|
||||
import { getDataDirKey, storeToken } from '../store';
|
||||
import { extractCode } from '../util';
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { getRedirectUrlViaExternalBrowser } from "../callbackServer";
|
||||
import { fetchAccessToken } from "../fetchAccessToken";
|
||||
import { getOrRefreshAccessToken } from "../getOrRefreshAccessToken";
|
||||
import type { AccessToken, TokenStoreArgs } from "../store";
|
||||
import { getDataDirKey, storeToken } from "../store";
|
||||
import { extractCode } from "../util";
|
||||
|
||||
export const PKCE_SHA256 = 'S256';
|
||||
export const PKCE_PLAIN = 'plain';
|
||||
export const PKCE_SHA256 = "S256";
|
||||
export const PKCE_PLAIN = "plain";
|
||||
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
||||
|
||||
export type CallbackType = 'localhost' | 'hosted';
|
||||
export type CallbackType = "localhost" | "hosted";
|
||||
|
||||
export interface ExternalBrowserOptions {
|
||||
useExternalBrowser: boolean;
|
||||
@@ -50,7 +50,7 @@ export async function getAuthorizationCode(
|
||||
challengeMethod: string;
|
||||
codeVerifier: string;
|
||||
} | null;
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
tokenName: "access_token" | "id_token";
|
||||
externalBrowser?: ExternalBrowserOptions;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
@@ -74,21 +74,21 @@ export async function getAuthorizationCode(
|
||||
|
||||
let authorizationUrl: URL;
|
||||
try {
|
||||
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
|
||||
} catch {
|
||||
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
|
||||
}
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
authorizationUrl.searchParams.set("response_type", "code");
|
||||
authorizationUrl.searchParams.set("client_id", clientId);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (audience) authorizationUrl.searchParams.set("audience", audience);
|
||||
if (pkce) {
|
||||
authorizationUrl.searchParams.set(
|
||||
'code_challenge',
|
||||
"code_challenge",
|
||||
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
|
||||
);
|
||||
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
|
||||
authorizationUrl.searchParams.set("code_challenge_method", pkce.challengeMethod);
|
||||
}
|
||||
|
||||
let code: string;
|
||||
@@ -103,21 +103,21 @@ export async function getAuthorizationCode(
|
||||
// Pass null to skip redirect URI matching — the callback came from our own local server
|
||||
const extractedCode = extractCode(result.callbackUrl, null);
|
||||
if (!extractedCode) {
|
||||
throw new Error('No authorization code found in callback URL');
|
||||
throw new Error("No authorization code found in callback URL");
|
||||
}
|
||||
code = extractedCode;
|
||||
actualRedirectUri = result.redirectUri;
|
||||
} else {
|
||||
// Use embedded browser flow (original behavior)
|
||||
if (redirectUri) {
|
||||
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
}
|
||||
code = await getCodeViaEmbeddedBrowser(ctx, contextId, authorizationUrl, redirectUri);
|
||||
}
|
||||
|
||||
console.log('[oauth2] Code found');
|
||||
console.log("[oauth2] Code found");
|
||||
const response = await fetchAccessToken(ctx, {
|
||||
grantType: 'authorization_code',
|
||||
grantType: "authorization_code",
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
@@ -125,9 +125,9 @@ export async function getAuthorizationCode(
|
||||
audience,
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'code', value: code },
|
||||
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
|
||||
...(actualRedirectUri ? [{ name: 'redirect_uri', value: actualRedirectUri }] : []),
|
||||
{ name: "code", value: code },
|
||||
...(pkce ? [{ name: "code_verifier", value: pkce.codeVerifier }] : []),
|
||||
...(actualRedirectUri ? [{ name: "redirect_uri", value: actualRedirectUri }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ async function getCodeViaEmbeddedBrowser(
|
||||
): Promise<string> {
|
||||
const dataDirKey = await getDataDirKey(ctx, contextId);
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log('[oauth2] Authorizing via embedded browser', authorizationUrlStr);
|
||||
console.log("[oauth2] Authorizing via embedded browser", authorizationUrlStr);
|
||||
|
||||
// oxlint-disable-next-line no-async-promise-executor -- Required for this pattern
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
@@ -154,10 +154,10 @@ async function getCodeViaEmbeddedBrowser(
|
||||
const { close } = await ctx.window.openUrl({
|
||||
dataDirKey,
|
||||
url: authorizationUrlStr,
|
||||
label: 'oauth-authorization-url',
|
||||
label: "oauth-authorization-url",
|
||||
async onClose() {
|
||||
if (!foundCode) {
|
||||
reject(new Error('Authorization window closed'));
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
@@ -187,21 +187,21 @@ export function genPkceCodeVerifier() {
|
||||
}
|
||||
|
||||
function pkceCodeChallenge(verifier: string, method: string) {
|
||||
if (method === 'plain') {
|
||||
if (method === "plain") {
|
||||
return verifier;
|
||||
}
|
||||
|
||||
const hash = encodeForPkce(createHash('sha256').update(verifier).digest());
|
||||
const hash = encodeForPkce(createHash("sha256").update(verifier).digest());
|
||||
return hash
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
.replace(/=/g, "") // Remove padding '='
|
||||
.replace(/\+/g, "-") // Replace '+' with '-'
|
||||
.replace(/\//g, "_"); // Replace '/' with '_'
|
||||
}
|
||||
|
||||
function encodeForPkce(bytes: Buffer) {
|
||||
return bytes
|
||||
.toString('base64')
|
||||
.replace(/=/g, '') // Remove padding '='
|
||||
.replace(/\+/g, '-') // Replace '+' with '-'
|
||||
.replace(/\//g, '_'); // Replace '/' with '_'
|
||||
.toString("base64")
|
||||
.replace(/=/g, "") // Remove padding '='
|
||||
.replace(/\+/g, "-") // Replace '+' with '-'
|
||||
.replace(/\//g, "_"); // Replace '/' with '_'
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { createPrivateKey, randomUUID } from 'node:crypto';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import jwt, { type Algorithm } from 'jsonwebtoken';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import type { TokenStoreArgs } from '../store';
|
||||
import { getToken, storeToken } from '../store';
|
||||
import { isTokenExpired } from '../util';
|
||||
import { createPrivateKey, randomUUID } from "node:crypto";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import jwt, { type Algorithm } from "jsonwebtoken";
|
||||
import { fetchAccessToken } from "../fetchAccessToken";
|
||||
import type { TokenStoreArgs } from "../store";
|
||||
import { getToken, storeToken } from "../store";
|
||||
import { isTokenExpired } from "../util";
|
||||
|
||||
export const jwtAlgorithms = [
|
||||
'HS256',
|
||||
'HS384',
|
||||
'HS512',
|
||||
'RS256',
|
||||
'RS384',
|
||||
'RS512',
|
||||
'PS256',
|
||||
'PS384',
|
||||
'PS512',
|
||||
'ES256',
|
||||
'ES384',
|
||||
'ES512',
|
||||
'none',
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"none",
|
||||
] as const;
|
||||
|
||||
export const defaultJwtAlgorithm = jwtAlgorithms[0];
|
||||
@@ -40,7 +40,7 @@ function buildClientAssertionJwt(params: {
|
||||
}): string {
|
||||
const { clientId, accessTokenUrl, secret, algorithm } = params;
|
||||
|
||||
const isHmac = algorithm.startsWith('HS') || algorithm === 'none';
|
||||
const isHmac = algorithm.startsWith("HS") || algorithm === "none";
|
||||
|
||||
// Resolve the signing key depending on format
|
||||
let signingKey: jwt.Secret;
|
||||
@@ -51,25 +51,25 @@ function buildClientAssertionJwt(params: {
|
||||
if (isHmac) {
|
||||
// HMAC algorithms use the raw secret (string or Buffer)
|
||||
signingKey = secret;
|
||||
} else if (trimmed.startsWith('{')) {
|
||||
} else if (trimmed.startsWith("{")) {
|
||||
// Looks like JSON - treat as JWK. There is surely a better way to detect JWK vs a raw secret, but this should work in most cases.
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
let jwk: any;
|
||||
try {
|
||||
jwk = JSON.parse(trimmed);
|
||||
} catch {
|
||||
throw new Error('Client Assertion secret looks like JSON but is not valid');
|
||||
throw new Error("Client Assertion secret looks like JSON but is not valid");
|
||||
}
|
||||
|
||||
kid = jwk?.kid;
|
||||
signingKey = createPrivateKey({ key: jwk, format: 'jwk' });
|
||||
} else if (trimmed.startsWith('-----')) {
|
||||
signingKey = createPrivateKey({ key: jwk, format: "jwk" });
|
||||
} else if (trimmed.startsWith("-----")) {
|
||||
// PEM-encoded key
|
||||
signingKey = createPrivateKey({ key: trimmed, format: 'pem' });
|
||||
signingKey = createPrivateKey({ key: trimmed, format: "pem" });
|
||||
} else {
|
||||
throw new Error(
|
||||
'Client Assertion secret must be a JWK JSON object, a PEM-encoded key ' +
|
||||
'(starting with -----), or a raw secret for HMAC algorithms.',
|
||||
"Client Assertion secret must be a JWK JSON object, a PEM-encoded key " +
|
||||
"(starting with -----), or a raw secret for HMAC algorithms.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ function buildClientAssertionJwt(params: {
|
||||
};
|
||||
|
||||
// Build the JWT header; include "kid" when available
|
||||
const header: jwt.JwtHeader = { alg: algorithm, typ: 'JWT' };
|
||||
const header: jwt.JwtHeader = { alg: algorithm, typ: "JWT" };
|
||||
if (kid) {
|
||||
header.kid = kid;
|
||||
}
|
||||
@@ -135,9 +135,9 @@ export async function getClientCredentials(
|
||||
|
||||
const common: Omit<
|
||||
Parameters<typeof fetchAccessToken>[1],
|
||||
'clientAssertion' | 'clientSecret' | 'credentialsInBody'
|
||||
"clientAssertion" | "clientSecret" | "credentialsInBody"
|
||||
> = {
|
||||
grantType: 'client_credentials',
|
||||
grantType: "client_credentials",
|
||||
accessTokenUrl,
|
||||
audience,
|
||||
clientId,
|
||||
@@ -146,7 +146,7 @@ export async function getClientCredentials(
|
||||
};
|
||||
|
||||
const fetchParams: Parameters<typeof fetchAccessToken>[1] =
|
||||
clientCredentialsMethod === 'client_assertion'
|
||||
clientCredentialsMethod === "client_assertion"
|
||||
? {
|
||||
...common,
|
||||
clientAssertion: buildClientAssertionJwt({
|
||||
@@ -154,7 +154,7 @@ export async function getClientCredentials(
|
||||
algorithm: clientAssertionAlgorithm as Algorithm,
|
||||
accessTokenUrl,
|
||||
secret: clientAssertionSecretBase64
|
||||
? Buffer.from(clientAssertionSecret, 'base64').toString('utf-8')
|
||||
? Buffer.from(clientAssertionSecret, "base64").toString("utf-8")
|
||||
: clientAssertionSecret,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { getRedirectUrlViaExternalBrowser } from '../callbackServer';
|
||||
import type { AccessToken, AccessTokenRawResponse } from '../store';
|
||||
import { getDataDirKey, getToken, storeToken } from '../store';
|
||||
import { isTokenExpired } from '../util';
|
||||
import type { ExternalBrowserOptions } from './authorizationCode';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { getRedirectUrlViaExternalBrowser } from "../callbackServer";
|
||||
import type { AccessToken, AccessTokenRawResponse } from "../store";
|
||||
import { getDataDirKey, getToken, storeToken } from "../store";
|
||||
import { isTokenExpired } from "../util";
|
||||
import type { ExternalBrowserOptions } from "./authorizationCode";
|
||||
|
||||
export async function getImplicit(
|
||||
ctx: Context,
|
||||
@@ -26,7 +26,7 @@ export async function getImplicit(
|
||||
scope: string | null;
|
||||
state: string | null;
|
||||
audience: string | null;
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
tokenName: "access_token" | "id_token";
|
||||
externalBrowser?: ExternalBrowserOptions;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
@@ -43,18 +43,18 @@ export async function getImplicit(
|
||||
|
||||
let authorizationUrl: URL;
|
||||
try {
|
||||
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
|
||||
authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
|
||||
} catch {
|
||||
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
|
||||
}
|
||||
authorizationUrl.searchParams.set('response_type', responseType);
|
||||
authorizationUrl.searchParams.set('client_id', clientId);
|
||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||
if (state) authorizationUrl.searchParams.set('state', state);
|
||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||
if (responseType.includes('id_token')) {
|
||||
authorizationUrl.searchParams.set("response_type", responseType);
|
||||
authorizationUrl.searchParams.set("client_id", clientId);
|
||||
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
||||
if (state) authorizationUrl.searchParams.set("state", state);
|
||||
if (audience) authorizationUrl.searchParams.set("audience", audience);
|
||||
if (responseType.includes("id_token")) {
|
||||
authorizationUrl.searchParams.set(
|
||||
'nonce',
|
||||
"nonce",
|
||||
String(Math.floor(Math.random() * 9999999999999) + 1),
|
||||
);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function getImplicit(
|
||||
} else {
|
||||
// Use embedded browser flow (original behavior)
|
||||
if (redirectUri) {
|
||||
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
}
|
||||
newToken = await getTokenViaEmbeddedBrowser(
|
||||
ctx,
|
||||
@@ -99,11 +99,11 @@ async function getTokenViaEmbeddedBrowser(
|
||||
accessTokenUrl: null;
|
||||
authorizationUrl: string;
|
||||
},
|
||||
tokenName: 'access_token' | 'id_token',
|
||||
tokenName: "access_token" | "id_token",
|
||||
): Promise<AccessToken> {
|
||||
const dataDirKey = await getDataDirKey(ctx, contextId);
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log('[oauth2] Authorizing via embedded browser (implicit)', authorizationUrlStr);
|
||||
console.log("[oauth2] Authorizing via embedded browser (implicit)", authorizationUrlStr);
|
||||
|
||||
// oxlint-disable-next-line no-async-promise-executor -- Required for this pattern
|
||||
return new Promise<AccessToken>(async (resolve, reject) => {
|
||||
@@ -111,16 +111,16 @@ async function getTokenViaEmbeddedBrowser(
|
||||
const { close } = await ctx.window.openUrl({
|
||||
dataDirKey,
|
||||
url: authorizationUrlStr,
|
||||
label: 'oauth-authorization-url',
|
||||
label: "oauth-authorization-url",
|
||||
async onClose() {
|
||||
if (!foundAccessToken) {
|
||||
reject(new Error('Authorization window closed'));
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has('error')) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get('error')}`));
|
||||
if (url.searchParams.has("error")) {
|
||||
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
|
||||
}
|
||||
|
||||
const hash = url.hash.slice(1);
|
||||
@@ -158,13 +158,13 @@ async function extractImplicitToken(
|
||||
accessTokenUrl: null;
|
||||
authorizationUrl: string;
|
||||
},
|
||||
tokenName: 'access_token' | 'id_token',
|
||||
tokenName: "access_token" | "id_token",
|
||||
): Promise<AccessToken> {
|
||||
const url = new URL(callbackUrl);
|
||||
|
||||
// Check for errors
|
||||
if (url.searchParams.has('error')) {
|
||||
throw new Error(`Failed to authorize: ${url.searchParams.get('error')}`);
|
||||
if (url.searchParams.has("error")) {
|
||||
throw new Error(`Failed to authorize: ${url.searchParams.get("error")}`);
|
||||
}
|
||||
|
||||
// Extract token from fragment
|
||||
@@ -179,18 +179,18 @@ async function extractImplicitToken(
|
||||
|
||||
// Build response from params (prefer fragment, fall back to query)
|
||||
const response: AccessTokenRawResponse = {
|
||||
access_token: params.get('access_token') ?? url.searchParams.get('access_token') ?? '',
|
||||
token_type: params.get('token_type') ?? url.searchParams.get('token_type') ?? undefined,
|
||||
expires_in: params.has('expires_in')
|
||||
? parseInt(params.get('expires_in') ?? '0', 10)
|
||||
: url.searchParams.has('expires_in')
|
||||
? parseInt(url.searchParams.get('expires_in') ?? '0', 10)
|
||||
access_token: params.get("access_token") ?? url.searchParams.get("access_token") ?? "",
|
||||
token_type: params.get("token_type") ?? url.searchParams.get("token_type") ?? undefined,
|
||||
expires_in: params.has("expires_in")
|
||||
? parseInt(params.get("expires_in") ?? "0", 10)
|
||||
: url.searchParams.has("expires_in")
|
||||
? parseInt(url.searchParams.get("expires_in") ?? "0", 10)
|
||||
: undefined,
|
||||
scope: params.get('scope') ?? url.searchParams.get('scope') ?? undefined,
|
||||
scope: params.get("scope") ?? url.searchParams.get("scope") ?? undefined,
|
||||
};
|
||||
|
||||
// Include id_token if present
|
||||
const idToken = params.get('id_token') ?? url.searchParams.get('id_token');
|
||||
const idToken = params.get("id_token") ?? url.searchParams.get("id_token");
|
||||
if (idToken) {
|
||||
response.id_token = idToken;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import type { AccessToken, TokenStoreArgs } from '../store';
|
||||
import { storeToken } from '../store';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { fetchAccessToken } from "../fetchAccessToken";
|
||||
import { getOrRefreshAccessToken } from "../getOrRefreshAccessToken";
|
||||
import type { AccessToken, TokenStoreArgs } from "../store";
|
||||
import { storeToken } from "../store";
|
||||
|
||||
export async function getPassword(
|
||||
ctx: Context,
|
||||
@@ -50,11 +50,11 @@ export async function getPassword(
|
||||
clientSecret,
|
||||
scope,
|
||||
audience,
|
||||
grantType: 'password',
|
||||
grantType: "password",
|
||||
credentialsInBody,
|
||||
params: [
|
||||
{ name: 'username', value: username },
|
||||
{ name: 'password', value: password },
|
||||
{ name: "username", value: username },
|
||||
{ name: "password", value: password },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import type {
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
import type { Algorithm } from 'jsonwebtoken';
|
||||
} from "@yaakapp/api";
|
||||
import type { Algorithm } from "jsonwebtoken";
|
||||
import {
|
||||
buildHostedCallbackRedirectUri,
|
||||
DEFAULT_LOCALHOST_PORT,
|
||||
stopActiveServer,
|
||||
} from './callbackServer';
|
||||
} from "./callbackServer";
|
||||
import {
|
||||
type CallbackType,
|
||||
DEFAULT_PKCE_METHOD,
|
||||
@@ -18,31 +18,31 @@ import {
|
||||
getAuthorizationCode,
|
||||
PKCE_PLAIN,
|
||||
PKCE_SHA256,
|
||||
} from './grants/authorizationCode';
|
||||
} from "./grants/authorizationCode";
|
||||
import {
|
||||
defaultJwtAlgorithm,
|
||||
getClientCredentials,
|
||||
jwtAlgorithms,
|
||||
} from './grants/clientCredentials';
|
||||
import { getImplicit } from './grants/implicit';
|
||||
import { getPassword } from './grants/password';
|
||||
import type { AccessToken, TokenStoreArgs } from './store';
|
||||
import { deleteToken, getToken, resetDataDirKey } from './store';
|
||||
} from "./grants/clientCredentials";
|
||||
import { getImplicit } from "./grants/implicit";
|
||||
import { getPassword } from "./grants/password";
|
||||
import type { AccessToken, TokenStoreArgs } from "./store";
|
||||
import { deleteToken, getToken, resetDataDirKey } from "./store";
|
||||
|
||||
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
|
||||
type GrantType = "authorization_code" | "implicit" | "password" | "client_credentials";
|
||||
|
||||
const grantTypes: FormInputSelectOption[] = [
|
||||
{ label: 'Authorization Code', value: 'authorization_code' },
|
||||
{ label: 'Implicit', value: 'implicit' },
|
||||
{ label: 'Resource Owner Password Credential', value: 'password' },
|
||||
{ label: 'Client Credentials', value: 'client_credentials' },
|
||||
{ label: "Authorization Code", value: "authorization_code" },
|
||||
{ label: "Implicit", value: "implicit" },
|
||||
{ label: "Resource Owner Password Credential", value: "password" },
|
||||
{ label: "Client Credentials", value: "client_credentials" },
|
||||
];
|
||||
|
||||
const defaultGrantType = grantTypes[0]?.value;
|
||||
|
||||
function hiddenIfNot(
|
||||
grantTypes: GrantType[],
|
||||
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
|
||||
...other: ((values: GetHttpAuthenticationConfigRequest["values"]) => boolean)[]
|
||||
) {
|
||||
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
|
||||
const hasGrantType = grantTypes.find((t) => t === String(values.grantType ?? defaultGrantType));
|
||||
@@ -53,37 +53,37 @@ function hiddenIfNot(
|
||||
}
|
||||
|
||||
const authorizationUrls = [
|
||||
'https://github.com/login/oauth/authorize',
|
||||
'https://account.box.com/api/oauth2/authorize',
|
||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
'https://api.imgur.com/oauth2/authorize',
|
||||
'https://bitly.com/oauth/authorize',
|
||||
'https://gitlab.example.com/oauth/authorize',
|
||||
'https://medium.com/m/oauth/authorize',
|
||||
'https://public-api.wordpress.com/oauth2/authorize',
|
||||
'https://slack.com/oauth/authorize',
|
||||
'https://todoist.com/oauth/authorize',
|
||||
'https://www.dropbox.com/oauth2/authorize',
|
||||
'https://www.linkedin.com/oauth/v2/authorization',
|
||||
'https://MY_SHOP.myshopify.com/admin/oauth/access_token',
|
||||
'https://appcenter.intuit.com/app/connect/oauth2/authorize',
|
||||
"https://github.com/login/oauth/authorize",
|
||||
"https://account.box.com/api/oauth2/authorize",
|
||||
"https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"https://api.imgur.com/oauth2/authorize",
|
||||
"https://bitly.com/oauth/authorize",
|
||||
"https://gitlab.example.com/oauth/authorize",
|
||||
"https://medium.com/m/oauth/authorize",
|
||||
"https://public-api.wordpress.com/oauth2/authorize",
|
||||
"https://slack.com/oauth/authorize",
|
||||
"https://todoist.com/oauth/authorize",
|
||||
"https://www.dropbox.com/oauth2/authorize",
|
||||
"https://www.linkedin.com/oauth/v2/authorization",
|
||||
"https://MY_SHOP.myshopify.com/admin/oauth/access_token",
|
||||
"https://appcenter.intuit.com/app/connect/oauth2/authorize",
|
||||
];
|
||||
|
||||
const accessTokenUrls = [
|
||||
'https://github.com/login/oauth/access_token',
|
||||
'https://api-ssl.bitly.com/oauth/access_token',
|
||||
'https://api.box.com/oauth2/token',
|
||||
'https://api.dropboxapi.com/oauth2/token',
|
||||
'https://api.imgur.com/oauth2/token',
|
||||
'https://api.medium.com/v1/tokens',
|
||||
'https://gitlab.example.com/oauth/token',
|
||||
'https://public-api.wordpress.com/oauth2/token',
|
||||
'https://slack.com/api/oauth.access',
|
||||
'https://todoist.com/oauth/access_token',
|
||||
'https://www.googleapis.com/oauth2/v4/token',
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
'https://MY_SHOP.myshopify.com/admin/oauth/authorize',
|
||||
'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
|
||||
"https://github.com/login/oauth/access_token",
|
||||
"https://api-ssl.bitly.com/oauth/access_token",
|
||||
"https://api.box.com/oauth2/token",
|
||||
"https://api.dropboxapi.com/oauth2/token",
|
||||
"https://api.imgur.com/oauth2/token",
|
||||
"https://api.medium.com/v1/tokens",
|
||||
"https://gitlab.example.com/oauth/token",
|
||||
"https://public-api.wordpress.com/oauth2/token",
|
||||
"https://slack.com/api/oauth.access",
|
||||
"https://todoist.com/oauth/access_token",
|
||||
"https://www.googleapis.com/oauth2/v4/token",
|
||||
"https://www.linkedin.com/oauth/v2/accessToken",
|
||||
"https://MY_SHOP.myshopify.com/admin/oauth/authorize",
|
||||
"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
||||
];
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
@@ -91,59 +91,59 @@ export const plugin: PluginDefinition = {
|
||||
stopActiveServer();
|
||||
},
|
||||
authentication: {
|
||||
name: 'oauth2',
|
||||
label: 'OAuth 2.0',
|
||||
shortLabel: 'OAuth 2',
|
||||
name: "oauth2",
|
||||
label: "OAuth 2.0",
|
||||
shortLabel: "OAuth 2",
|
||||
actions: [
|
||||
{
|
||||
label: 'Copy Current Token',
|
||||
label: "Copy Current Token",
|
||||
async onSelect(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
authorizationUrl: stringArg(values, "authorizationUrl"),
|
||||
accessTokenUrl: stringArg(values, "accessTokenUrl"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token == null) {
|
||||
await ctx.toast.show({
|
||||
message: 'No token to copy',
|
||||
color: 'warning',
|
||||
message: "No token to copy",
|
||||
color: "warning",
|
||||
});
|
||||
} else {
|
||||
await ctx.clipboard.copyText(token.response.access_token);
|
||||
await ctx.toast.show({
|
||||
message: 'Token copied to clipboard',
|
||||
icon: 'copy',
|
||||
color: 'success',
|
||||
message: "Token copied to clipboard",
|
||||
icon: "copy",
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete Token',
|
||||
label: "Delete Token",
|
||||
async onSelect(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
authorizationUrl: stringArg(values, "authorizationUrl"),
|
||||
accessTokenUrl: stringArg(values, "accessTokenUrl"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
};
|
||||
if (await deleteToken(ctx, tokenArgs)) {
|
||||
await ctx.toast.show({
|
||||
message: 'Token deleted',
|
||||
color: 'success',
|
||||
message: "Token deleted",
|
||||
color: "success",
|
||||
});
|
||||
} else {
|
||||
await ctx.toast.show({
|
||||
message: 'No token to delete',
|
||||
color: 'warning',
|
||||
message: "No token to delete",
|
||||
color: "warning",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clear Window Session',
|
||||
label: "Clear Window Session",
|
||||
async onSelect(ctx, { contextId }) {
|
||||
await resetDataDirKey(ctx, contextId);
|
||||
},
|
||||
@@ -151,85 +151,85 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'grantType',
|
||||
label: 'Grant Type',
|
||||
type: "select",
|
||||
name: "grantType",
|
||||
label: "Grant Type",
|
||||
defaultValue: defaultGrantType,
|
||||
options: grantTypes,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'clientCredentialsMethod',
|
||||
label: 'Authentication Method',
|
||||
type: "select",
|
||||
name: "clientCredentialsMethod",
|
||||
label: "Authentication Method",
|
||||
description:
|
||||
'"Client Secret" sends client_secret. \n' + '"Client Assertion" sends a signed JWT.',
|
||||
defaultValue: 'client_secret',
|
||||
defaultValue: "client_secret",
|
||||
options: [
|
||||
{ label: 'Client Secret', value: 'client_secret' },
|
||||
{ label: 'Client Assertion', value: 'client_assertion' },
|
||||
{ label: "Client Secret", value: "client_secret" },
|
||||
{ label: "Client Assertion", value: "client_assertion" },
|
||||
],
|
||||
dynamic: hiddenIfNot(['client_credentials']),
|
||||
dynamic: hiddenIfNot(["client_credentials"]),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: "text",
|
||||
name: "clientId",
|
||||
label: "Client ID",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: "text",
|
||||
name: "clientSecret",
|
||||
label: "Client Secret",
|
||||
optional: true,
|
||||
password: true,
|
||||
dynamic: hiddenIfNot(
|
||||
['authorization_code', 'password', 'client_credentials'],
|
||||
(values) => values.clientCredentialsMethod === 'client_secret',
|
||||
["authorization_code", "password", "client_credentials"],
|
||||
(values) => values.clientCredentialsMethod === "client_secret",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'clientAssertionAlgorithm',
|
||||
label: 'JWT Algorithm',
|
||||
type: "select",
|
||||
name: "clientAssertionAlgorithm",
|
||||
label: "JWT Algorithm",
|
||||
defaultValue: defaultJwtAlgorithm,
|
||||
options: jwtAlgorithms.map((value) => ({
|
||||
label: value === 'none' ? 'None' : value,
|
||||
label: value === "none" ? "None" : value,
|
||||
value,
|
||||
})),
|
||||
dynamic: hiddenIfNot(
|
||||
['client_credentials'],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === 'client_assertion',
|
||||
["client_credentials"],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === "client_assertion",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'clientAssertionSecret',
|
||||
label: 'JWT Secret',
|
||||
type: "text",
|
||||
name: "clientAssertionSecret",
|
||||
label: "JWT Secret",
|
||||
description:
|
||||
'Can be HMAC, PEM or JWK. Make sure you pick the correct algorithm type above.',
|
||||
"Can be HMAC, PEM or JWK. Make sure you pick the correct algorithm type above.",
|
||||
password: true,
|
||||
optional: true,
|
||||
multiLine: true,
|
||||
dynamic: hiddenIfNot(
|
||||
['client_credentials'],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === 'client_assertion',
|
||||
["client_credentials"],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === "client_assertion",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'clientAssertionSecretBase64',
|
||||
label: 'JWT secret is base64 encoded',
|
||||
type: "checkbox",
|
||||
name: "clientAssertionSecretBase64",
|
||||
label: "JWT secret is base64 encoded",
|
||||
dynamic: hiddenIfNot(
|
||||
['client_credentials'],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === 'client_assertion',
|
||||
["client_credentials"],
|
||||
({ clientCredentialsMethod }) => clientCredentialsMethod === "client_assertion",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'authorizationUrl',
|
||||
type: "text",
|
||||
name: "authorizationUrl",
|
||||
optional: true,
|
||||
label: 'Authorization URL',
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
label: "Authorization URL",
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||
placeholder: authorizationUrls[0],
|
||||
completionOptions: authorizationUrls.map((url) => ({
|
||||
label: url,
|
||||
@@ -237,103 +237,103 @@ export const plugin: PluginDefinition = {
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'accessTokenUrl',
|
||||
type: "text",
|
||||
name: "accessTokenUrl",
|
||||
optional: true,
|
||||
label: 'Access Token URL',
|
||||
label: "Access Token URL",
|
||||
placeholder: accessTokenUrls[0],
|
||||
dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']),
|
||||
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
|
||||
completionOptions: accessTokenUrls.map((url) => ({
|
||||
label: url,
|
||||
value: url,
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'banner',
|
||||
type: "banner",
|
||||
inputs: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'useExternalBrowser',
|
||||
label: 'Use External Browser',
|
||||
type: "checkbox",
|
||||
name: "useExternalBrowser",
|
||||
label: "Use External Browser",
|
||||
description:
|
||||
'Open authorization URL in your system browser instead of the embedded browser. ' +
|
||||
'Useful when the OAuth provider blocks embedded browsers or you need existing browser sessions.',
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
"Open authorization URL in your system browser instead of the embedded browser. " +
|
||||
"Useful when the OAuth provider blocks embedded browsers or you need existing browser sessions.",
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'redirectUri',
|
||||
label: 'Redirect URI (can be any valid URL)',
|
||||
placeholder: 'https://mysite.example.com/oauth/callback',
|
||||
type: "text",
|
||||
name: "redirectUri",
|
||||
label: "Redirect URI (can be any valid URL)",
|
||||
placeholder: "https://mysite.example.com/oauth/callback",
|
||||
description:
|
||||
'URI the OAuth provider redirects to after authorization. Yaak intercepts this automatically in its embedded browser so any valid URI will work.',
|
||||
"URI the OAuth provider redirects to after authorization. Yaak intercepts this automatically in its embedded browser so any valid URI will work.",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(
|
||||
['authorization_code', 'implicit'],
|
||||
["authorization_code", "implicit"],
|
||||
({ useExternalBrowser }) => !useExternalBrowser,
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'callbackType',
|
||||
label: 'Callback Type',
|
||||
type: "select",
|
||||
name: "callbackType",
|
||||
label: "Callback Type",
|
||||
description:
|
||||
'"Hosted Redirect" uses an external Yaak-hosted endpoint. "Localhost" starts a local server to receive the callback.',
|
||||
defaultValue: 'hosted',
|
||||
defaultValue: "hosted",
|
||||
options: [
|
||||
{ label: 'Hosted Redirect', value: 'hosted' },
|
||||
{ label: 'Localhost', value: 'localhost' },
|
||||
{ label: "Hosted Redirect", value: "hosted" },
|
||||
{ label: "Localhost", value: "localhost" },
|
||||
],
|
||||
dynamic: hiddenIfNot(
|
||||
['authorization_code', 'implicit'],
|
||||
["authorization_code", "implicit"],
|
||||
({ useExternalBrowser }) => !!useExternalBrowser,
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'callbackPort',
|
||||
label: 'Callback Port',
|
||||
type: "text",
|
||||
name: "callbackPort",
|
||||
label: "Callback Port",
|
||||
placeholder: `${DEFAULT_LOCALHOST_PORT}`,
|
||||
description:
|
||||
'Port for the local callback server. Defaults to ' +
|
||||
"Port for the local callback server. Defaults to " +
|
||||
DEFAULT_LOCALHOST_PORT +
|
||||
' if empty.',
|
||||
" if empty.",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(
|
||||
['authorization_code', 'implicit'],
|
||||
["authorization_code", "implicit"],
|
||||
({ useExternalBrowser }) => !!useExternalBrowser,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'banner',
|
||||
color: 'info',
|
||||
type: "banner",
|
||||
color: "info",
|
||||
inputs: [
|
||||
{
|
||||
type: 'markdown',
|
||||
content: 'Redirect URI to Register',
|
||||
type: "markdown",
|
||||
content: "Redirect URI to Register",
|
||||
async dynamic(_ctx, { values }) {
|
||||
const grantType = String(values.grantType ?? defaultGrantType);
|
||||
const useExternalBrowser = !!values.useExternalBrowser;
|
||||
const callbackType = (stringArg(values, 'callbackType') ||
|
||||
'localhost') as CallbackType;
|
||||
const callbackType = (stringArg(values, "callbackType") ||
|
||||
"localhost") as CallbackType;
|
||||
|
||||
// Only show for authorization_code and implicit with external browser enabled
|
||||
if (
|
||||
!['authorization_code', 'implicit'].includes(grantType) ||
|
||||
!["authorization_code", "implicit"].includes(grantType) ||
|
||||
!useExternalBrowser
|
||||
) {
|
||||
return { hidden: true };
|
||||
}
|
||||
|
||||
// Compute the redirect URI based on callback type
|
||||
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
||||
const port = intArg(values, "callbackPort") || DEFAULT_LOCALHOST_PORT;
|
||||
let redirectUri: string;
|
||||
if (callbackType === 'hosted') {
|
||||
if (callbackType === "hosted") {
|
||||
redirectUri = buildHostedCallbackRedirectUri(port);
|
||||
} else {
|
||||
redirectUri = `http://127.0.0.1:${port}/callback`;
|
||||
@@ -350,149 +350,149 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
type: "text",
|
||||
name: "state",
|
||||
label: "State",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||
},
|
||||
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
|
||||
{ type: 'text', name: 'audience', label: 'Audience', optional: true },
|
||||
{ type: "text", name: "scope", label: "Scope", optional: true },
|
||||
{ type: "text", name: "audience", label: "Audience", optional: true },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'tokenName',
|
||||
label: 'Token for authorization',
|
||||
type: "select",
|
||||
name: "tokenName",
|
||||
label: "Token for authorization",
|
||||
description:
|
||||
'Select which token to send in the "Authorization: Bearer" header. Most APIs expect ' +
|
||||
'access_token, but some (like OpenID Connect) require id_token.',
|
||||
defaultValue: 'access_token',
|
||||
"access_token, but some (like OpenID Connect) require id_token.",
|
||||
defaultValue: "access_token",
|
||||
options: [
|
||||
{ label: 'access_token', value: 'access_token' },
|
||||
{ label: 'id_token', value: 'id_token' },
|
||||
{ label: "access_token", value: "access_token" },
|
||||
{ label: "id_token", value: "id_token" },
|
||||
],
|
||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||
},
|
||||
{
|
||||
type: 'banner',
|
||||
type: "banner",
|
||||
inputs: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'usePkce',
|
||||
label: 'Use PKCE',
|
||||
dynamic: hiddenIfNot(['authorization_code']),
|
||||
type: "checkbox",
|
||||
name: "usePkce",
|
||||
label: "Use PKCE",
|
||||
dynamic: hiddenIfNot(["authorization_code"]),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'pkceChallengeMethod',
|
||||
label: 'Code Challenge Method',
|
||||
type: "select",
|
||||
name: "pkceChallengeMethod",
|
||||
label: "Code Challenge Method",
|
||||
options: [
|
||||
{ label: 'SHA-256', value: PKCE_SHA256 },
|
||||
{ label: 'Plain', value: PKCE_PLAIN },
|
||||
{ label: "SHA-256", value: PKCE_SHA256 },
|
||||
{ label: "Plain", value: PKCE_PLAIN },
|
||||
],
|
||||
defaultValue: DEFAULT_PKCE_METHOD,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pkceCodeChallenge',
|
||||
label: 'Code Verifier',
|
||||
placeholder: 'Automatically generated when not set',
|
||||
type: "text",
|
||||
name: "pkceCodeChallenge",
|
||||
label: "Code Verifier",
|
||||
placeholder: "Automatically generated when not set",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
type: "text",
|
||||
name: "username",
|
||||
label: "Username",
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
dynamic: hiddenIfNot(["password"]),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: "text",
|
||||
name: "password",
|
||||
label: "Password",
|
||||
password: true,
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(['password']),
|
||||
dynamic: hiddenIfNot(["password"]),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'responseType',
|
||||
label: 'Response Type',
|
||||
defaultValue: 'token',
|
||||
type: "select",
|
||||
name: "responseType",
|
||||
label: "Response Type",
|
||||
defaultValue: "token",
|
||||
options: [
|
||||
{ label: 'Access Token', value: 'token' },
|
||||
{ label: 'ID Token', value: 'id_token' },
|
||||
{ label: 'ID and Access Token', value: 'id_token token' },
|
||||
{ label: "Access Token", value: "token" },
|
||||
{ label: "ID Token", value: "id_token" },
|
||||
{ label: "ID and Access Token", value: "id_token token" },
|
||||
],
|
||||
dynamic: hiddenIfNot(['implicit']),
|
||||
dynamic: hiddenIfNot(["implicit"]),
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerName',
|
||||
label: 'Header Name',
|
||||
defaultValue: 'Authorization',
|
||||
type: "text",
|
||||
name: "headerName",
|
||||
label: "Header Name",
|
||||
defaultValue: "Authorization",
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerPrefix',
|
||||
label: 'Header Prefix',
|
||||
type: "text",
|
||||
name: "headerPrefix",
|
||||
label: "Header Prefix",
|
||||
optional: true,
|
||||
defaultValue: 'Bearer',
|
||||
defaultValue: "Bearer",
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'credentials',
|
||||
label: 'Send Credentials',
|
||||
defaultValue: 'body',
|
||||
type: "select",
|
||||
name: "credentials",
|
||||
label: "Send Credentials",
|
||||
defaultValue: "body",
|
||||
options: [
|
||||
{ label: 'In Request Body', value: 'body' },
|
||||
{ label: 'As Basic Authentication', value: 'basic' },
|
||||
{ label: "In Request Body", value: "body" },
|
||||
{ label: "As Basic Authentication", value: "basic" },
|
||||
],
|
||||
dynamic: (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => ({
|
||||
hidden:
|
||||
values.grantType === 'client_credentials' &&
|
||||
values.clientCredentialsMethod === 'client_assertion',
|
||||
values.grantType === "client_credentials" &&
|
||||
values.clientCredentialsMethod === "client_assertion",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Access Token Response',
|
||||
type: "accordion",
|
||||
label: "Access Token Response",
|
||||
inputs: [],
|
||||
async dynamic(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
authorizationUrl: stringArg(values, "authorizationUrl"),
|
||||
accessTokenUrl: stringArg(values, "accessTokenUrl"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token == null) {
|
||||
return { hidden: true };
|
||||
}
|
||||
return {
|
||||
label: 'Access Token Response',
|
||||
label: "Access Token Response",
|
||||
inputs: [
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'response',
|
||||
type: "editor",
|
||||
name: "response",
|
||||
defaultValue: JSON.stringify(token.response, null, 2),
|
||||
hideLabel: true,
|
||||
readOnly: true,
|
||||
language: 'json',
|
||||
language: "json",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -500,101 +500,101 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
],
|
||||
async onApply(ctx, { values, contextId }) {
|
||||
const headerPrefix = stringArg(values, 'headerPrefix');
|
||||
const grantType = stringArg(values, 'grantType') as GrantType;
|
||||
const credentialsInBody = values.credentials === 'body';
|
||||
const tokenName = values.tokenName === 'id_token' ? 'id_token' : 'access_token';
|
||||
const headerPrefix = stringArg(values, "headerPrefix");
|
||||
const grantType = stringArg(values, "grantType") as GrantType;
|
||||
const credentialsInBody = values.credentials === "body";
|
||||
const tokenName = values.tokenName === "id_token" ? "id_token" : "access_token";
|
||||
|
||||
// Build external browser options if enabled
|
||||
const useExternalBrowser = !!values.useExternalBrowser;
|
||||
const externalBrowserOptions = useExternalBrowser
|
||||
? {
|
||||
useExternalBrowser: true,
|
||||
callbackType: (stringArg(values, 'callbackType') || 'localhost') as CallbackType,
|
||||
callbackPort: intArg(values, 'callbackPort') ?? undefined,
|
||||
callbackType: (stringArg(values, "callbackType") || "localhost") as CallbackType,
|
||||
callbackPort: intArg(values, "callbackPort") ?? undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let token: AccessToken;
|
||||
if (grantType === 'authorization_code') {
|
||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
if (grantType === "authorization_code") {
|
||||
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getAuthorizationCode(ctx, contextId, {
|
||||
accessTokenUrl:
|
||||
accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//)
|
||||
accessTokenUrl === "" || accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
authorizationUrl:
|
||||
authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
|
||||
authorizationUrl === "" || authorizationUrl.match(/^https?:\/\//)
|
||||
? authorizationUrl
|
||||
: `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
state: stringArgOrNull(values, 'state'),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
state: stringArgOrNull(values, "state"),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce
|
||||
? {
|
||||
challengeMethod: stringArg(values, 'pkceChallengeMethod') || DEFAULT_PKCE_METHOD,
|
||||
codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(),
|
||||
challengeMethod: stringArg(values, "pkceChallengeMethod") || DEFAULT_PKCE_METHOD,
|
||||
codeVerifier: stringArg(values, "pkceCodeVerifier") || genPkceCodeVerifier(),
|
||||
}
|
||||
: null,
|
||||
tokenName: tokenName,
|
||||
externalBrowser: externalBrowserOptions,
|
||||
});
|
||||
} else if (grantType === 'implicit') {
|
||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||
} else if (grantType === "implicit") {
|
||||
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||
token = await getImplicit(ctx, contextId, {
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//)
|
||||
? authorizationUrl
|
||||
: `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
responseType: stringArg(values, 'responseType'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
state: stringArgOrNull(values, 'state'),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
responseType: stringArg(values, "responseType"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
state: stringArgOrNull(values, "state"),
|
||||
tokenName: tokenName,
|
||||
externalBrowser: externalBrowserOptions,
|
||||
});
|
||||
} else if (grantType === 'client_credentials') {
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
} else if (grantType === "client_credentials") {
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getClientCredentials(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientAssertionAlgorithm: stringArg(values, 'clientAssertionAlgorithm') as Algorithm,
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
clientCredentialsMethod: stringArg(values, 'clientCredentialsMethod'),
|
||||
clientAssertionSecret: stringArg(values, 'clientAssertionSecret'),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientAssertionAlgorithm: stringArg(values, "clientAssertionAlgorithm") as Algorithm,
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
clientCredentialsMethod: stringArg(values, "clientCredentialsMethod"),
|
||||
clientAssertionSecret: stringArg(values, "clientAssertionSecret"),
|
||||
clientAssertionSecretBase64: !!values.clientAssertionSecretBase64,
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
credentialsInBody,
|
||||
});
|
||||
} else if (grantType === 'password') {
|
||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||
} else if (grantType === "password") {
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getPassword(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
username: stringArg(values, 'username'),
|
||||
password: stringArg(values, 'password'),
|
||||
scope: stringArgOrNull(values, 'scope'),
|
||||
audience: stringArgOrNull(values, 'audience'),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
username: stringArg(values, "username"),
|
||||
password: stringArg(values, "password"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
audience: stringArgOrNull(values, "audience"),
|
||||
credentialsInBody,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Invalid grant type ${String(grantType)}`);
|
||||
}
|
||||
|
||||
const headerName = stringArg(values, 'headerName') || 'Authorization';
|
||||
const headerValue = `${headerPrefix} ${token.response[tokenName] ?? ''}`.trim();
|
||||
const headerName = stringArg(values, "headerName") || "Authorization";
|
||||
const headerValue = `${headerPrefix} ${token.response[tokenName] ?? ""}`.trim();
|
||||
return { setHeaders: [{ name: headerName, value: headerValue }] };
|
||||
},
|
||||
},
|
||||
@@ -605,19 +605,19 @@ function stringArgOrNull(
|
||||
name: string,
|
||||
): string | null {
|
||||
const arg = values[name];
|
||||
if (arg == null || arg === '') return null;
|
||||
if (arg == null || arg === "") return null;
|
||||
return `${arg}`;
|
||||
}
|
||||
|
||||
function stringArg(values: Record<string, JsonPrimitive | undefined>, name: string): string {
|
||||
const arg = stringArgOrNull(values, name);
|
||||
if (!arg) return '';
|
||||
if (!arg) return "";
|
||||
return arg;
|
||||
}
|
||||
|
||||
function intArg(values: Record<string, JsonPrimitive | undefined>, name: string): number | null {
|
||||
const arg = values[name];
|
||||
if (arg == null || arg === '') return null;
|
||||
if (arg == null || arg === "") return null;
|
||||
const num = parseInt(`${arg}`, 10);
|
||||
return Number.isNaN(num) ? null : num;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
|
||||
export async function storeToken(
|
||||
ctx: Context,
|
||||
args: TokenStoreArgs,
|
||||
response: AccessTokenRawResponse,
|
||||
tokenName: 'access_token' | 'id_token' = 'access_token',
|
||||
tokenName: "access_token" | "id_token" = "access_token",
|
||||
) {
|
||||
if (!response[tokenName]) {
|
||||
throw new Error(`${tokenName} not found in response ${Object.keys(response).join(', ')}`);
|
||||
throw new Error(`${tokenName} not found in response ${Object.keys(response).join(", ")}`);
|
||||
}
|
||||
|
||||
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1000 : null;
|
||||
@@ -34,7 +34,7 @@ export async function resetDataDirKey(ctx: Context, contextId: string) {
|
||||
}
|
||||
|
||||
export async function getDataDirKey(ctx: Context, contextId: string) {
|
||||
const key = (await ctx.store.get<string>(dataDirStoreKey(contextId))) ?? 'default';
|
||||
const key = (await ctx.store.get<string>(dataDirStoreKey(contextId))) ?? "default";
|
||||
return `${contextId}::${key}`;
|
||||
}
|
||||
|
||||
@@ -50,17 +50,17 @@ export interface TokenStoreArgs {
|
||||
* account for slight variations (like domains with and without a protocol scheme).
|
||||
*/
|
||||
function tokenStoreKey(args: TokenStoreArgs) {
|
||||
const hash = createHash('md5');
|
||||
const hash = createHash("md5");
|
||||
if (args.contextId) hash.update(args.contextId.trim());
|
||||
if (args.clientId) hash.update(args.clientId.trim());
|
||||
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
|
||||
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
|
||||
const key = hash.digest('hex');
|
||||
return ['token', key].join('::');
|
||||
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ""));
|
||||
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ""));
|
||||
const key = hash.digest("hex");
|
||||
return ["token", key].join("::");
|
||||
}
|
||||
|
||||
function dataDirStoreKey(contextId: string) {
|
||||
return ['data_dir', contextId].join('::');
|
||||
return ["data_dir", contextId].join("::");
|
||||
}
|
||||
|
||||
export interface AccessToken {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AccessToken } from './store';
|
||||
import type { AccessToken } from "./store";
|
||||
|
||||
export function isTokenExpired(token: AccessToken) {
|
||||
return token.expiresAt && Date.now() > token.expiresAt;
|
||||
@@ -8,24 +8,24 @@ export function extractCode(urlStr: string, redirectUri: string | null): string
|
||||
const url = new URL(urlStr);
|
||||
|
||||
if (!urlMatchesRedirect(url, redirectUri)) {
|
||||
console.log('[oauth2] URL does not match redirect origin/path; skipping.');
|
||||
console.log("[oauth2] URL does not match redirect origin/path; skipping.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefer query param; fall back to fragment if query lacks it
|
||||
|
||||
const query = url.searchParams;
|
||||
const queryError = query.get('error');
|
||||
const queryDesc = query.get('error_description');
|
||||
const queryUri = query.get('error_uri');
|
||||
const queryError = query.get("error");
|
||||
const queryDesc = query.get("error_description");
|
||||
const queryUri = query.get("error_uri");
|
||||
|
||||
let hashParams: URLSearchParams | null = null;
|
||||
if (url.hash && url.hash.length > 1) {
|
||||
hashParams = new URLSearchParams(url.hash.slice(1));
|
||||
}
|
||||
const hashError = hashParams?.get('error');
|
||||
const hashDesc = hashParams?.get('error_description');
|
||||
const hashUri = hashParams?.get('error_uri');
|
||||
const hashError = hashParams?.get("error");
|
||||
const hashDesc = hashParams?.get("error_description");
|
||||
const hashUri = hashParams?.get("error_uri");
|
||||
|
||||
const error = queryError || hashError;
|
||||
if (error) {
|
||||
@@ -37,13 +37,13 @@ export function extractCode(urlStr: string, redirectUri: string | null): string
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const queryCode = query.get('code');
|
||||
const queryCode = query.get("code");
|
||||
if (queryCode) return queryCode;
|
||||
|
||||
const hashCode = hashParams?.get('code');
|
||||
const hashCode = hashParams?.get("code");
|
||||
if (hashCode) return hashCode;
|
||||
|
||||
console.log('[oauth2] Code not found');
|
||||
console.log("[oauth2] Code not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolea
|
||||
try {
|
||||
redirect = new URL(redirectUrl);
|
||||
} catch {
|
||||
console.log('[oauth2] Invalid redirect URI; skipping.');
|
||||
console.log("[oauth2] Invalid redirect URI; skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,17 +63,17 @@ export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolea
|
||||
const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase();
|
||||
|
||||
const normalizePort = (u: URL) =>
|
||||
(u.protocol === 'https:' && (!u.port || u.port === '443')) ||
|
||||
(u.protocol === 'http:' && (!u.port || u.port === '80'))
|
||||
? ''
|
||||
(u.protocol === "https:" && (!u.port || u.port === "443")) ||
|
||||
(u.protocol === "http:" && (!u.port || u.port === "80"))
|
||||
? ""
|
||||
: u.port;
|
||||
|
||||
const samePort = normalizePort(url) === normalizePort(redirect);
|
||||
|
||||
const normPath = (p: string) => {
|
||||
const withLeading = p.startsWith('/') ? p : `/${p}`;
|
||||
const withLeading = p.startsWith("/") ? p : `/${p}`;
|
||||
// strip trailing slashes, keep root as "/"
|
||||
return withLeading.replace(/\/+$/g, '') || '/';
|
||||
return withLeading.replace(/\/+$/g, "") || "/";
|
||||
};
|
||||
|
||||
// Require redirect path to be a prefix of the navigated URL path
|
||||
|
||||
@@ -1,109 +1,109 @@
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { extractCode } from '../src/util';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { extractCode } from "../src/util";
|
||||
|
||||
describe('extractCode', () => {
|
||||
test('extracts code from query when same origin + path', () => {
|
||||
const url = 'https://app.example.com/cb?code=abc123&state=xyz';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('abc123');
|
||||
describe("extractCode", () => {
|
||||
test("extracts code from query when same origin + path", () => {
|
||||
const url = "https://app.example.com/cb?code=abc123&state=xyz";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("abc123");
|
||||
});
|
||||
|
||||
test('extracts code from query with weird path', () => {
|
||||
const url = 'https://app.example.com/cbwithextra?code=abc123&state=xyz';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
test("extracts code from query with weird path", () => {
|
||||
const url = "https://app.example.com/cbwithextra?code=abc123&state=xyz";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBeNull();
|
||||
});
|
||||
|
||||
test('allows trailing slash differences', () => {
|
||||
expect(extractCode('https://app.example.com/cb/?code=abc', 'https://app.example.com/cb')).toBe(
|
||||
'abc',
|
||||
test("allows trailing slash differences", () => {
|
||||
expect(extractCode("https://app.example.com/cb/?code=abc", "https://app.example.com/cb")).toBe(
|
||||
"abc",
|
||||
);
|
||||
expect(extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com/cb/')).toBe(
|
||||
'abc',
|
||||
expect(extractCode("https://app.example.com/cb?code=abc", "https://app.example.com/cb/")).toBe(
|
||||
"abc",
|
||||
);
|
||||
});
|
||||
|
||||
test('treats default ports as equal (https:443, http:80)', () => {
|
||||
test("treats default ports as equal (https:443, http:80)", () => {
|
||||
expect(
|
||||
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:443/cb'),
|
||||
).toBe('abc');
|
||||
expect(extractCode('http://app.example.com/cb?code=abc', 'http://app.example.com:80/cb')).toBe(
|
||||
'abc',
|
||||
extractCode("https://app.example.com/cb?code=abc", "https://app.example.com:443/cb"),
|
||||
).toBe("abc");
|
||||
expect(extractCode("http://app.example.com/cb?code=abc", "http://app.example.com:80/cb")).toBe(
|
||||
"abc",
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects different port', () => {
|
||||
test("rejects different port", () => {
|
||||
expect(
|
||||
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:8443/cb'),
|
||||
extractCode("https://app.example.com/cb?code=abc", "https://app.example.com:8443/cb"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('rejects different hostname (including subdomain changes)', () => {
|
||||
test("rejects different hostname (including subdomain changes)", () => {
|
||||
expect(
|
||||
extractCode('https://evil.example.com/cb?code=abc', 'https://app.example.com/cb'),
|
||||
extractCode("https://evil.example.com/cb?code=abc", "https://app.example.com/cb"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('requires path to start with redirect path (ignoring query/hash)', () => {
|
||||
test("requires path to start with redirect path (ignoring query/hash)", () => {
|
||||
// same origin but wrong path -> null
|
||||
expect(
|
||||
extractCode('https://app.example.com/other?code=abc', 'https://app.example.com/cb'),
|
||||
extractCode("https://app.example.com/other?code=abc", "https://app.example.com/cb"),
|
||||
).toBeNull();
|
||||
|
||||
// deeper subpath under the redirect path -> allowed (prefix match)
|
||||
expect(
|
||||
extractCode('https://app.example.com/cb/deep?code=abc', 'https://app.example.com/cb'),
|
||||
).toBe('abc');
|
||||
extractCode("https://app.example.com/cb/deep?code=abc", "https://app.example.com/cb"),
|
||||
).toBe("abc");
|
||||
});
|
||||
|
||||
test('works with custom schemes', () => {
|
||||
expect(extractCode('myapp://cb?code=abc', 'myapp://cb')).toBe('abc');
|
||||
test("works with custom schemes", () => {
|
||||
expect(extractCode("myapp://cb?code=abc", "myapp://cb")).toBe("abc");
|
||||
});
|
||||
|
||||
test('prefers query over fragment when both present', () => {
|
||||
const url = 'https://app.example.com/cb?code=queryCode#code=hashCode';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('queryCode');
|
||||
test("prefers query over fragment when both present", () => {
|
||||
const url = "https://app.example.com/cb?code=queryCode#code=hashCode";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("queryCode");
|
||||
});
|
||||
|
||||
test('extracts code from fragment when query lacks code', () => {
|
||||
const url = 'https://app.example.com/cb#code=fromHash&state=xyz';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('fromHash');
|
||||
test("extracts code from fragment when query lacks code", () => {
|
||||
const url = "https://app.example.com/cb#code=fromHash&state=xyz";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("fromHash");
|
||||
});
|
||||
|
||||
test('returns null if no code present (query or fragment)', () => {
|
||||
const url = 'https://app.example.com/cb?state=only';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
test("returns null if no code present (query or fragment)", () => {
|
||||
const url = "https://app.example.com/cb?state=only";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when provider reports an error', () => {
|
||||
const url = 'https://app.example.com/cb?error=access_denied&error_description=oopsy';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(() => extractCode(url, redirect)).toThrow('Failed to authorize: access_denied');
|
||||
test("returns null when provider reports an error", () => {
|
||||
const url = "https://app.example.com/cb?error=access_denied&error_description=oopsy";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(() => extractCode(url, redirect)).toThrow("Failed to authorize: access_denied");
|
||||
});
|
||||
|
||||
test('when redirectUri is null, extracts code from any URL', () => {
|
||||
expect(extractCode('https://random.example.com/whatever?code=abc', null)).toBe('abc');
|
||||
test("when redirectUri is null, extracts code from any URL", () => {
|
||||
expect(extractCode("https://random.example.com/whatever?code=abc", null)).toBe("abc");
|
||||
});
|
||||
|
||||
test('handles extra params gracefully', () => {
|
||||
const url = 'https://app.example.com/cb?foo=1&bar=2&code=abc&baz=3';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('abc');
|
||||
test("handles extra params gracefully", () => {
|
||||
const url = "https://app.example.com/cb?foo=1&bar=2&code=abc&baz=3";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("abc");
|
||||
});
|
||||
|
||||
test('ignores fragment noise when code is in query', () => {
|
||||
const url = 'https://app.example.com/cb?code=abc#some=thing';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('abc');
|
||||
test("ignores fragment noise when code is in query", () => {
|
||||
const url = "https://app.example.com/cb?code=abc#some=thing";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("abc");
|
||||
});
|
||||
|
||||
// If you decide NOT to support fragment-based codes, flip these to expect null or mark as .skip
|
||||
test('supports fragment-only code for response_mode=fragment providers', () => {
|
||||
const url = 'https://app.example.com/cb#state=xyz&code=abc';
|
||||
const redirect = 'https://app.example.com/cb';
|
||||
expect(extractCode(url, redirect)).toBe('abc');
|
||||
test("supports fragment-only code for response_mode=fragment providers", () => {
|
||||
const url = "https://app.example.com/cb#state=xyz&code=abc";
|
||||
const redirect = "https://app.example.com/cb";
|
||||
expect(extractCode(url, redirect)).toBe("abc");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@yaak/filter-jsonpath",
|
||||
"displayName": "JSONPath Filter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Filter JSON response data using JSONPath expressions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins/filter-jsonpath"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
filter: {
|
||||
name: 'JSONPath',
|
||||
description: 'Filter JSONPath',
|
||||
name: "JSONPath",
|
||||
description: "Filter JSONPath",
|
||||
onFilter(_ctx, args) {
|
||||
const parsed = JSON.parse(args.payload);
|
||||
try {
|
||||
const filtered = JSONPath({ path: args.filter, json: parsed });
|
||||
return { content: JSON.stringify(filtered, null, 2) };
|
||||
} catch (err) {
|
||||
return { content: '', error: `Invalid filter: ${err instanceof Error ? err.message : String(err)}` };
|
||||
return {
|
||||
content: "",
|
||||
error: `Invalid filter: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/filter-xpath",
|
||||
"displayName": "XPath Filter",
|
||||
"description": "Filter response XML data using XPath expressions",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Filter response XML data using XPath expressions",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
/* oxlint-disable no-base-to-string */
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import xpath from 'xpath';
|
||||
import { DOMParser } from "@xmldom/xmldom";
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import xpath from "xpath";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
filter: {
|
||||
name: 'XPath',
|
||||
description: 'Filter XPath',
|
||||
name: "XPath",
|
||||
description: "Filter XPath",
|
||||
onFilter(_ctx, args) {
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const doc: any = new DOMParser().parseFromString(args.payload, 'text/xml');
|
||||
const doc: any = new DOMParser().parseFromString(args.payload, "text/xml");
|
||||
try {
|
||||
const result = xpath.select(args.filter, doc, false);
|
||||
if (Array.isArray(result)) {
|
||||
return { content: result.map((r) => String(r)).join('\n') };
|
||||
return { content: result.map((r) => String(r)).join("\n") };
|
||||
}
|
||||
// Not sure what cases this happens in (?)
|
||||
return { content: String(result) };
|
||||
} catch (err) {
|
||||
return { content: '', error: `Invalid filter: ${err instanceof Error ? err.message : String(err)}` };
|
||||
return {
|
||||
content: "",
|
||||
error: `Invalid filter: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-curl",
|
||||
"displayName": "cURL Importer",
|
||||
"description": "Import requests from cURL commands",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import requests from cURL commands",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -6,38 +6,38 @@ import type {
|
||||
HttpUrlParameter,
|
||||
PluginDefinition,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
import { split } from 'shlex';
|
||||
} from "@yaakapp/api";
|
||||
import { split } from "shlex";
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
||||
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId">[];
|
||||
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
||||
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
||||
}
|
||||
|
||||
const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii'];
|
||||
const DATA_FLAGS = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"];
|
||||
const SUPPORTED_FLAGS = [
|
||||
['cookie', 'b'],
|
||||
['d', 'data'], // Add url encoded data
|
||||
['data-ascii'],
|
||||
['data-binary'],
|
||||
['data-raw'],
|
||||
['data-urlencode'],
|
||||
['digest'], // Apply auth as digest
|
||||
['form', 'F'], // Add multipart data
|
||||
['get', 'G'], // Put the post data in the URL
|
||||
['header', 'H'],
|
||||
['request', 'X'], // Request method
|
||||
['url'], // Specify the URL explicitly
|
||||
['url-query'],
|
||||
['user', 'u'], // Authentication
|
||||
["cookie", "b"],
|
||||
["d", "data"], // Add url encoded data
|
||||
["data-ascii"],
|
||||
["data-binary"],
|
||||
["data-raw"],
|
||||
["data-urlencode"],
|
||||
["digest"], // Apply auth as digest
|
||||
["form", "F"], // Add multipart data
|
||||
["get", "G"], // Put the post data in the URL
|
||||
["header", "H"],
|
||||
["request", "X"], // Request method
|
||||
["url"], // Specify the URL explicitly
|
||||
["url-query"],
|
||||
["user", "u"], // Authentication
|
||||
DATA_FLAGS,
|
||||
].flat();
|
||||
|
||||
const BOOLEAN_FLAGS = ['G', 'get', 'digest'];
|
||||
const BOOLEAN_FLAGS = ["G", "get", "digest"];
|
||||
|
||||
type FlagValue = string | boolean;
|
||||
|
||||
@@ -45,8 +45,8 @@ type FlagsByName = Record<string, FlagValue[]>;
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'cURL',
|
||||
description: 'Import cURL commands',
|
||||
name: "cURL",
|
||||
description: "Import cURL commands",
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
return convertCurl(args.text) as any;
|
||||
@@ -60,14 +60,14 @@ export const plugin: PluginDefinition = {
|
||||
*/
|
||||
function splitCommands(rawData: string): string[] {
|
||||
// Join line continuations (backslash-newline, and backslash-CRLF for Windows)
|
||||
const joined = rawData.replace(/\\\r?\n/g, ' ');
|
||||
const joined = rawData.replace(/\\\r?\n/g, " ");
|
||||
|
||||
// Count consecutive backslashes immediately before position i.
|
||||
// An even count means the quote at i is NOT escaped; odd means it IS escaped.
|
||||
function isEscaped(i: number): boolean {
|
||||
let backslashes = 0;
|
||||
let j = i - 1;
|
||||
while (j >= 0 && joined[j] === '\\') {
|
||||
while (j >= 0 && joined[j] === "\\") {
|
||||
backslashes++;
|
||||
j--;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ function splitCommands(rawData: string): string[] {
|
||||
|
||||
// Split on semicolons and newlines to separate commands
|
||||
const commands: string[] = [];
|
||||
let current = '';
|
||||
let current = "";
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let inDollarQuote = false;
|
||||
@@ -108,7 +108,7 @@ function splitCommands(rawData: string): string[] {
|
||||
current += ch;
|
||||
continue;
|
||||
}
|
||||
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && ch === '$' && next === "'") {
|
||||
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && ch === "$" && next === "'") {
|
||||
inDollarQuote = true;
|
||||
current += ch + next;
|
||||
i++; // Skip the opening quote
|
||||
@@ -126,13 +126,13 @@ function splitCommands(rawData: string): string[] {
|
||||
if (
|
||||
!inQuote &&
|
||||
!isEscaped(i) &&
|
||||
(ch === ';' || ch === '\n' || (ch === '\r' && next === '\n'))
|
||||
(ch === ";" || ch === "\n" || (ch === "\r" && next === "\n"))
|
||||
) {
|
||||
if (ch === '\r') i++; // Skip the \n in \r\n
|
||||
if (ch === "\r") i++; // Skip the \n in \r\n
|
||||
if (current.trim()) {
|
||||
commands.push(current.trim());
|
||||
}
|
||||
current = '';
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -156,21 +156,21 @@ export function convertCurl(rawData: string) {
|
||||
|
||||
// Break up squished arguments like `-XPOST` into `-X POST`
|
||||
return tokens.flatMap((token) => {
|
||||
if (token.startsWith('-') && !token.startsWith('--') && token.length > 2) {
|
||||
if (token.startsWith("-") && !token.startsWith("--") && token.length > 2) {
|
||||
return [token.slice(0, 2), token.slice(2)];
|
||||
}
|
||||
return token;
|
||||
});
|
||||
});
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: generateId('workspace'),
|
||||
name: 'Curl Import',
|
||||
const workspace: ExportResources["workspaces"][0] = {
|
||||
model: "workspace",
|
||||
id: generateId("workspace"),
|
||||
name: "Curl Import",
|
||||
};
|
||||
|
||||
const requests: ExportResources['httpRequests'] = commands
|
||||
.filter((command) => command[0] === 'curl')
|
||||
const requests: ExportResources["httpRequests"] = commands
|
||||
.filter((command) => command[0] === "curl")
|
||||
.map((v) => importCommand(v, workspace.id));
|
||||
|
||||
return {
|
||||
@@ -191,13 +191,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Start at 1 so we can skip the ^curl part
|
||||
for (let i = 1; i < parseEntries.length; i++) {
|
||||
let parseEntry = parseEntries[i];
|
||||
if (typeof parseEntry === 'string') {
|
||||
if (typeof parseEntry === "string") {
|
||||
parseEntry = parseEntry.trim();
|
||||
}
|
||||
|
||||
if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-';
|
||||
let name = parseEntry.replace(/^-{1,2}/, '');
|
||||
if (typeof parseEntry === "string" && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === "-" && parseEntry[1] !== "-";
|
||||
let name = parseEntry.replace(/^-{1,2}/, "");
|
||||
|
||||
if (!SUPPORTED_FLAGS.includes(name)) {
|
||||
continue;
|
||||
@@ -211,13 +211,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// - Double dash followed by a letter: --data-raw, --header
|
||||
// This prevents mistaking data that starts with dashes (like multipart boundaries ------) as flags
|
||||
const nextEntryIsFlag =
|
||||
typeof nextEntry === 'string' &&
|
||||
typeof nextEntry === "string" &&
|
||||
(nextEntry.match(/^-[a-zA-Z]/) || nextEntry.match(/^--[a-zA-Z]/));
|
||||
if (isSingleDash && name.length > 1) {
|
||||
// Handle squished arguments like -XPOST
|
||||
value = name.slice(1);
|
||||
name = name.slice(0, 1);
|
||||
} else if (typeof nextEntry === 'string' && hasValue && !nextEntryIsFlag) {
|
||||
} else if (typeof nextEntry === "string" && hasValue && !nextEntryIsFlag) {
|
||||
// Next arg is not a flag, so assign it as the value
|
||||
value = nextEntry;
|
||||
i++; // Skip next one
|
||||
@@ -236,14 +236,14 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Build the request //
|
||||
// ~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
|
||||
const [baseUrl, search] = splitOnce(urlArg, '?');
|
||||
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || "", ["url"]);
|
||||
const [baseUrl, search] = splitOnce(urlArg, "?");
|
||||
const urlParameters: HttpUrlParameter[] =
|
||||
search?.split('&').map((p) => {
|
||||
const v = splitOnce(p, '=');
|
||||
search?.split("&").map((p) => {
|
||||
const v = splitOnce(p, "=");
|
||||
return {
|
||||
name: decodeURIComponent(v[0] ?? ''),
|
||||
value: decodeURIComponent(v[1] ?? ''),
|
||||
name: decodeURIComponent(v[0] ?? ""),
|
||||
value: decodeURIComponent(v[1] ?? ""),
|
||||
enabled: true,
|
||||
};
|
||||
}) ?? [];
|
||||
@@ -251,27 +251,27 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
const url = baseUrl ?? urlArg;
|
||||
|
||||
// Query params
|
||||
for (const p of flagsByName['url-query'] ?? []) {
|
||||
if (typeof p !== 'string') {
|
||||
for (const p of flagsByName["url-query"] ?? []) {
|
||||
if (typeof p !== "string") {
|
||||
continue;
|
||||
}
|
||||
const [name, value] = p.split('=');
|
||||
const [name, value] = p.split("=");
|
||||
urlParameters.push({
|
||||
name: name ?? '',
|
||||
value: value ?? '',
|
||||
name: name ?? "",
|
||||
value: value ?? "",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication
|
||||
const [username, password] = getPairValue(flagsByName, '', ['u', 'user']).split(/:(.*)$/);
|
||||
const [username, password] = getPairValue(flagsByName, "", ["u", "user"]).split(/:(.*)$/);
|
||||
|
||||
const isDigest = getPairValue(flagsByName, false, ['digest']);
|
||||
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
|
||||
const isDigest = getPairValue(flagsByName, false, ["digest"]);
|
||||
const authenticationType = username ? (isDigest ? "digest" : "basic") : null;
|
||||
const authentication = username
|
||||
? {
|
||||
username: username.trim(),
|
||||
password: (password ?? '').trim(),
|
||||
password: (password ?? "").trim(),
|
||||
}
|
||||
: {};
|
||||
|
||||
@@ -284,13 +284,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// remove final colon from header name if present
|
||||
if (!value) {
|
||||
return {
|
||||
name: (name ?? '').trim().replace(/;$/, ''),
|
||||
value: '',
|
||||
name: (name ?? "").trim().replace(/;$/, ""),
|
||||
value: "",
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: (name ?? '').trim(),
|
||||
name: (name ?? "").trim(),
|
||||
value: value.trim(),
|
||||
enabled: true,
|
||||
};
|
||||
@@ -302,14 +302,14 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
...((flagsByName.b as string[] | undefined) || []),
|
||||
]
|
||||
.map((str) => {
|
||||
const name = str.split('=', 1)[0];
|
||||
const value = str.replace(`${name}=`, '');
|
||||
const name = str.split("=", 1)[0];
|
||||
const value = str.replace(`${name}=`, "");
|
||||
return `${name}=${value}`;
|
||||
})
|
||||
.join('; ');
|
||||
.join("; ");
|
||||
|
||||
// Convert cookie value to header
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie');
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === "cookie");
|
||||
|
||||
if (cookieHeaderValue && existingCookieHeader) {
|
||||
// Has existing cookie header, so let's update it
|
||||
@@ -317,15 +317,15 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
} else if (cookieHeaderValue) {
|
||||
// No existing cookie header, so let's make a new one
|
||||
headers.push({
|
||||
name: 'Cookie',
|
||||
name: "Cookie",
|
||||
value: cookieHeaderValue,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Body (Text or Blob)
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0]?.trim() : null;
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0]?.trim() : null;
|
||||
|
||||
// Extract boundary from Content-Type header for multipart parsing
|
||||
const boundaryMatch = contentTypeHeader?.value.match(/boundary=([^\s;]+)/i);
|
||||
@@ -333,19 +333,19 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
|
||||
// Get raw data from --data-raw flags (before splitting by &)
|
||||
const rawDataValues = [
|
||||
...((flagsByName['data-raw'] as string[] | undefined) || []),
|
||||
...((flagsByName["data-raw"] as string[] | undefined) || []),
|
||||
...((flagsByName.d as string[] | undefined) || []),
|
||||
...((flagsByName.data as string[] | undefined) || []),
|
||||
...((flagsByName['data-binary'] as string[] | undefined) || []),
|
||||
...((flagsByName['data-ascii'] as string[] | undefined) || []),
|
||||
...((flagsByName["data-binary"] as string[] | undefined) || []),
|
||||
...((flagsByName["data-ascii"] as string[] | undefined) || []),
|
||||
];
|
||||
|
||||
// Check if this is multipart form data in --data-raw (Chrome DevTools format)
|
||||
let multipartFormDataFromRaw:
|
||||
| { name: string; value?: string; file?: string; enabled: boolean }[]
|
||||
| null = null;
|
||||
if (mimeType === 'multipart/form-data' && boundary && rawDataValues.length > 0) {
|
||||
const rawBody = rawDataValues.join('');
|
||||
if (mimeType === "multipart/form-data" && boundary && rawDataValues.length > 0) {
|
||||
const rawBody = rawDataValues.join("");
|
||||
multipartFormDataFromRaw = parseMultipartFormData(rawBody, boundary);
|
||||
}
|
||||
|
||||
@@ -356,15 +356,15 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
...((flagsByName.form as string[] | undefined) || []),
|
||||
...((flagsByName.F as string[] | undefined) || []),
|
||||
].map((str) => {
|
||||
const parts = str.split('=');
|
||||
const name = parts[0] ?? '';
|
||||
const value = parts[1] ?? '';
|
||||
const parts = str.split("=");
|
||||
const name = parts[0] ?? "";
|
||||
const value = parts[1] ?? "";
|
||||
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
|
||||
name,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (value.indexOf('@') === 0) {
|
||||
if (value.indexOf("@") === 0) {
|
||||
item.file = value.slice(1);
|
||||
} else {
|
||||
item.value = value;
|
||||
@@ -376,11 +376,11 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Body
|
||||
let body = {};
|
||||
let bodyType: string | null = null;
|
||||
const bodyAsGET = getPairValue(flagsByName, false, ['G', 'get']);
|
||||
const bodyAsGET = getPairValue(flagsByName, false, ["G", "get"]);
|
||||
|
||||
if (multipartFormDataFromRaw) {
|
||||
// Handle multipart form data parsed from --data-raw (Chrome DevTools format)
|
||||
bodyType = 'multipart/form-data';
|
||||
bodyType = "multipart/form-data";
|
||||
body = {
|
||||
form: multipartFormDataFromRaw,
|
||||
};
|
||||
@@ -388,57 +388,57 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
urlParameters.push(...dataParameters);
|
||||
} else if (
|
||||
dataParameters.length > 0 &&
|
||||
(mimeType == null || mimeType === 'application/x-www-form-urlencoded')
|
||||
(mimeType == null || mimeType === "application/x-www-form-urlencoded")
|
||||
) {
|
||||
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
|
||||
bodyType = mimeType ?? "application/x-www-form-urlencoded";
|
||||
body = {
|
||||
form: dataParameters.map((parameter) => ({
|
||||
...parameter,
|
||||
name: decodeURIComponent(parameter.name || ''),
|
||||
value: decodeURIComponent(parameter.value || ''),
|
||||
name: decodeURIComponent(parameter.name || ""),
|
||||
value: decodeURIComponent(parameter.value || ""),
|
||||
})),
|
||||
};
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
});
|
||||
} else if (dataParameters.length > 0) {
|
||||
bodyType =
|
||||
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
|
||||
mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain"
|
||||
? mimeType
|
||||
: 'other';
|
||||
: "other";
|
||||
body = {
|
||||
text: dataParameters
|
||||
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
|
||||
.join('&'),
|
||||
.join("&"),
|
||||
};
|
||||
} else if (formDataParams.length) {
|
||||
bodyType = mimeType ?? 'multipart/form-data';
|
||||
bodyType = mimeType ?? "multipart/form-data";
|
||||
body = {
|
||||
form: formDataParams,
|
||||
};
|
||||
if (mimeType == null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method
|
||||
let method = getPairValue(flagsByName, '', ['X', 'request']).toUpperCase();
|
||||
let method = getPairValue(flagsByName, "", ["X", "request"]).toUpperCase();
|
||||
|
||||
if (method === '' && body) {
|
||||
method = 'text' in body || 'form' in body ? 'POST' : 'GET';
|
||||
if (method === "" && body) {
|
||||
method = "text" in body || "form" in body ? "POST" : "GET";
|
||||
}
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
id: generateId('http_request'),
|
||||
model: 'http_request',
|
||||
const request: ExportResources["httpRequests"][0] = {
|
||||
id: generateId("http_request"),
|
||||
model: "http_request",
|
||||
workspaceId,
|
||||
name: '',
|
||||
name: "",
|
||||
urlParameters,
|
||||
url,
|
||||
method,
|
||||
@@ -473,22 +473,22 @@ function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
|
||||
}
|
||||
|
||||
for (const p of pairs) {
|
||||
if (typeof p !== 'string') continue;
|
||||
const params = p.split('&');
|
||||
if (typeof p !== "string") continue;
|
||||
const params = p.split("&");
|
||||
for (const param of params) {
|
||||
const [name, value] = splitOnce(param, '=');
|
||||
if (param.startsWith('@')) {
|
||||
const [name, value] = splitOnce(param, "=");
|
||||
if (param.startsWith("@")) {
|
||||
// Yaak doesn't support files in url-encoded data, so
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: '',
|
||||
name: name ?? "",
|
||||
value: "",
|
||||
filePath: param.slice(1),
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : (value ?? ''),
|
||||
name: name ?? "",
|
||||
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : (value ?? ""),
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
@@ -537,12 +537,12 @@ function parseMultipartFormData(
|
||||
|
||||
for (const part of parts) {
|
||||
// Skip empty parts and the closing boundary marker
|
||||
if (!part || part.trim() === '--' || part.trim() === '--\r\n') {
|
||||
if (!part || part.trim() === "--" || part.trim() === "--\r\n") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each part has headers and content separated by \r\n\r\n
|
||||
const headerContentSplit = part.indexOf('\r\n\r\n');
|
||||
const headerContentSplit = part.indexOf("\r\n\r\n");
|
||||
if (headerContentSplit === -1) {
|
||||
continue;
|
||||
}
|
||||
@@ -551,7 +551,7 @@ function parseMultipartFormData(
|
||||
let content = part.slice(headerContentSplit + 4); // Skip \r\n\r\n
|
||||
|
||||
// Remove trailing \r\n from content
|
||||
if (content.endsWith('\r\n')) {
|
||||
if (content.endsWith("\r\n")) {
|
||||
content = content.slice(0, -2);
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ function parseMultipartFormData(
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = contentDispositionMatch[1] ?? '';
|
||||
const name = contentDispositionMatch[1] ?? "";
|
||||
const filename = contentDispositionMatch[2];
|
||||
|
||||
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
|
||||
|
||||
@@ -1,159 +1,182 @@
|
||||
import type { HttpRequest, Workspace } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convertCurl } from '../src';
|
||||
import type { HttpRequest, Workspace } from "@yaakapp/api";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertCurl } from "../src";
|
||||
|
||||
describe('importer-curl', () => {
|
||||
test('Imports basic GET', () => {
|
||||
expect(convertCurl('curl https://yaak.app')).toEqual({
|
||||
describe("importer-curl", () => {
|
||||
test("Imports basic GET", () => {
|
||||
expect(convertCurl("curl https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Explicit URL', () => {
|
||||
expect(convertCurl('curl --url https://yaak.app')).toEqual({
|
||||
test("Explicit URL", () => {
|
||||
expect(convertCurl("curl --url https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Missing URL', () => {
|
||||
expect(convertCurl('curl -X POST')).toEqual({
|
||||
test("Missing URL", () => {
|
||||
expect(convertCurl("curl -X POST")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('URL between', () => {
|
||||
expect(convertCurl('curl -v https://yaak.app -X POST')).toEqual({
|
||||
test("URL between", () => {
|
||||
expect(convertCurl("curl -v https://yaak.app -X POST")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Random flags', () => {
|
||||
expect(convertCurl('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
|
||||
test("Random flags", () => {
|
||||
expect(convertCurl("curl --random -Z -Y -S --foo https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports --request method', () => {
|
||||
expect(convertCurl('curl --request POST https://yaak.app')).toEqual({
|
||||
test("Imports --request method", () => {
|
||||
expect(convertCurl("curl --request POST https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports -XPOST method', () => {
|
||||
expect(convertCurl('curl -XPOST --request POST https://yaak.app')).toEqual({
|
||||
test("Imports -XPOST method", () => {
|
||||
expect(convertCurl("curl -XPOST --request POST https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple requests', () => {
|
||||
test("Imports multiple requests", () => {
|
||||
expect(
|
||||
convertCurl('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({ url: 'https://yaak.app' }),
|
||||
baseRequest({ url: 'example.com' }),
|
||||
baseRequest({ url: 'foo.com' }),
|
||||
baseRequest({ url: "https://yaak.app" }),
|
||||
baseRequest({ url: "example.com" }),
|
||||
baseRequest({ url: "foo.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports with Windows CRLF line endings', () => {
|
||||
expect(
|
||||
convertCurl('curl \\\r\n -X POST \\\r\n https://yaak.app'),
|
||||
).toEqual({
|
||||
test("Imports with Windows CRLF line endings", () => {
|
||||
expect(convertCurl("curl \\\r\n -X POST \\\r\n https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({ url: 'https://yaak.app', method: 'POST' }),
|
||||
],
|
||||
httpRequests: [baseRequest({ url: "https://yaak.app", method: "POST" })],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Throws on malformed quotes', () => {
|
||||
expect(() =>
|
||||
convertCurl('curl -X POST -F "a=aaa" -F b=bbb" https://yaak.app'),
|
||||
).toThrow();
|
||||
test("Throws on malformed quotes", () => {
|
||||
expect(() => convertCurl('curl -X POST -F "a=aaa" -F b=bbb" https://yaak.app')).toThrow();
|
||||
});
|
||||
|
||||
test('Imports form data', () => {
|
||||
expect(
|
||||
convertCurl('curl -X POST -F "a=aaa" -F b=bbb -F f=@filepath https://yaak.app'),
|
||||
).toEqual({
|
||||
test("Imports form data", () => {
|
||||
expect(convertCurl('curl -X POST -F "a=aaa" -F b=bbb -F f=@filepath https://yaak.app')).toEqual(
|
||||
{
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ enabled: true, name: "a", value: "aaa" },
|
||||
{ enabled: true, name: "b", value: "bbb" },
|
||||
{ enabled: true, name: "f", file: "filepath" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Imports data params as form url-encoded", () => {
|
||||
expect(convertCurl("curl -d a -d b -d c=ccc https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: [
|
||||
{ enabled: true, name: 'a', value: 'aaa' },
|
||||
{ enabled: true, name: 'b', value: 'bbb' },
|
||||
{ enabled: true, name: 'f', file: 'filepath' },
|
||||
{ name: "a", value: "", enabled: true },
|
||||
{ name: "b", value: "", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -162,56 +185,27 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as form url-encoded', () => {
|
||||
expect(convertCurl('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: '', enabled: true },
|
||||
{ name: 'b', value: '', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports combined data params as form url-encoded', () => {
|
||||
test("Imports combined data params as form url-encoded", () => {
|
||||
expect(convertCurl(`curl -d 'a=aaa&b=bbb&c' https://yaak.app`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa', enabled: true },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: '', enabled: true },
|
||||
{ name: "a", value: "aaa", enabled: true },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -220,38 +214,38 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as text', () => {
|
||||
test("Imports data params as text", () => {
|
||||
expect(
|
||||
convertCurl('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
|
||||
convertCurl("curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app"),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }],
|
||||
bodyType: 'text/plain',
|
||||
body: { text: 'a&b&c=ccc' },
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Content-Type", value: "text/plain", enabled: true }],
|
||||
bodyType: "text/plain",
|
||||
body: { text: "a&b&c=ccc" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports post data into URL', () => {
|
||||
expect(convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3')).toEqual({
|
||||
test("Imports post data into URL", () => {
|
||||
expect(convertCurl("curl -G https://api.stripe.com/v1/payment_links -d limit=3")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.stripe.com/v1/payment_links',
|
||||
method: "GET",
|
||||
url: "https://api.stripe.com/v1/payment_links",
|
||||
urlParameters: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'limit',
|
||||
value: '3',
|
||||
name: "limit",
|
||||
value: "3",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -260,7 +254,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multi-line JSON', () => {
|
||||
test("Imports multi-line JSON", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
|
||||
@@ -270,10 +264,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{\n "foo":"bar"\n}' },
|
||||
}),
|
||||
],
|
||||
@@ -281,20 +275,20 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple headers', () => {
|
||||
test("Imports multiple headers", () => {
|
||||
expect(
|
||||
convertCurl('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
|
||||
convertCurl("curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app"),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
headers: [
|
||||
{ name: 'Name', value: '', enabled: true },
|
||||
{ name: 'Foo', value: 'bar', enabled: true },
|
||||
{ name: 'AAA', value: 'bbb', enabled: true },
|
||||
{ name: '', value: 'ccc', enabled: true },
|
||||
{ name: "Name", value: "", enabled: true },
|
||||
{ name: "Foo", value: "bar", enabled: true },
|
||||
{ name: "AAA", value: "bbb", enabled: true },
|
||||
{ name: "", value: "ccc", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -302,17 +296,17 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports basic auth', () => {
|
||||
expect(convertCurl('curl --user user:pass https://yaak.app')).toEqual({
|
||||
test("Imports basic auth", () => {
|
||||
expect(convertCurl("curl --user user:pass https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -320,17 +314,17 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports digest auth', () => {
|
||||
expect(convertCurl('curl --digest --user user:pass https://yaak.app')).toEqual({
|
||||
test("Imports digest auth", () => {
|
||||
expect(convertCurl("curl --digest --user user:pass https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'digest',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "digest",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -338,30 +332,30 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports cookie as header', () => {
|
||||
test("Imports cookie as header", () => {
|
||||
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }],
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Cookie", value: "foo=bar", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params', () => {
|
||||
test("Imports query params", () => {
|
||||
expect(convertCurl('curl "https://yaak.app" --url-query foo=bar --url-query baz=qux')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'qux', enabled: true },
|
||||
{ name: "foo", value: "bar", enabled: true },
|
||||
{ name: "baz", value: "qux", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -369,16 +363,16 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params from the URL', () => {
|
||||
test("Imports query params from the URL", () => {
|
||||
expect(convertCurl('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'a a', enabled: true },
|
||||
{ name: "foo", value: "bar", enabled: true },
|
||||
{ name: "baz", value: "a a", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -386,23 +380,23 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports weird body', () => {
|
||||
test("Imports weird body", () => {
|
||||
expect(convertCurl(`curl 'https://yaak.app' -X POST --data-raw 'foo=bar=baz'`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'foo', value: 'bar=baz', enabled: true }],
|
||||
form: [{ name: "foo", value: "bar=baz", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -411,7 +405,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data with Unicode escape sequences', () => {
|
||||
test("Imports data with Unicode escape sequences", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{"query":"SearchQueryInput\\u0021"}' -X POST`,
|
||||
@@ -421,10 +415,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{"query":"SearchQueryInput!"}' },
|
||||
}),
|
||||
],
|
||||
@@ -432,7 +426,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data with multiple escape sequences', () => {
|
||||
test("Imports data with multiple escape sequences", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' --data-raw $'Line1\\nLine2\\tTab\\u0021Exclamation' -X POST`,
|
||||
@@ -442,17 +436,17 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'Line1\nLine2\tTab!Exclamation', value: '', enabled: true }],
|
||||
form: [{ name: "Line1\nLine2\tTab!Exclamation", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -461,7 +455,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multipart form data from --data-raw (Chrome DevTools format)', () => {
|
||||
test("Imports multipart form data from --data-raw (Chrome DevTools format)", () => {
|
||||
// This is the format Chrome DevTools uses when copying a multipart form submission as cURL
|
||||
const curlCommand = `curl 'http://localhost:8080/system' \
|
||||
-H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd' \
|
||||
@@ -472,21 +466,21 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'http://localhost:8080/system',
|
||||
method: 'POST',
|
||||
url: "http://localhost:8080/system",
|
||||
method: "POST",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'username', value: 'jsgj', enabled: true },
|
||||
{ name: 'password', value: '654321', enabled: true },
|
||||
{ name: 'captcha', file: 'test.xlsx', enabled: true },
|
||||
{ name: "username", value: "jsgj", enabled: true },
|
||||
{ name: "password", value: "654321", enabled: true },
|
||||
{ name: "captcha", file: "test.xlsx", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -495,7 +489,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports JSON body with newlines in $quotes', () => {
|
||||
test("Imports JSON body with newlines in $quotes", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{\\n "foo": "bar",\\n "baz": "qux"\\n}' -X POST`,
|
||||
@@ -505,10 +499,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{\n "foo": "bar",\n "baz": "qux"\n}' },
|
||||
}),
|
||||
],
|
||||
@@ -516,110 +510,94 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles double-quoted string ending with even backslashes before semicolon', () => {
|
||||
test("Handles double-quoted string ending with even backslashes before semicolon", () => {
|
||||
// "C:\\" has two backslashes which escape each other, so the closing " is real.
|
||||
// The ; after should split into a second command.
|
||||
expect(
|
||||
convertCurl(
|
||||
'curl -d "C:\\\\" https://yaak.app;curl https://example.com',
|
||||
),
|
||||
).toEqual({
|
||||
expect(convertCurl('curl -d "C:\\\\" https://yaak.app;curl https://example.com')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'C:\\', value: '', enabled: true }],
|
||||
form: [{ name: "C:\\", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
baseRequest({ url: 'https://example.com' }),
|
||||
baseRequest({ url: "https://example.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles $quoted string ending with a literal backslash before semicolon', () => {
|
||||
test("Handles $quoted string ending with a literal backslash before semicolon", () => {
|
||||
// $'C:\\\\' has two backslashes which become one literal backslash.
|
||||
// The closing ' must not be misinterpreted as escaped.
|
||||
// The ; after should split into a second command.
|
||||
expect(
|
||||
convertCurl(
|
||||
"curl -d $'C:\\\\' https://yaak.app;curl https://example.com",
|
||||
),
|
||||
).toEqual({
|
||||
expect(convertCurl("curl -d $'C:\\\\' https://yaak.app;curl https://example.com")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'C:\\', value: '', enabled: true }],
|
||||
form: [{ name: "C:\\", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
baseRequest({ url: 'https://example.com' }),
|
||||
baseRequest({ url: "https://example.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports $quoted header with escaped single quotes', () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl https://yaak.app -H $'X-Custom: it\\'s a test'`,
|
||||
),
|
||||
).toEqual({
|
||||
test("Imports $quoted header with escaped single quotes", () => {
|
||||
expect(convertCurl(`curl https://yaak.app -H $'X-Custom: it\\'s a test'`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'X-Custom', value: "it's a test", enabled: true }],
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "X-Custom", value: "it's a test", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Does not split on escaped semicolon outside quotes', () => {
|
||||
test("Does not split on escaped semicolon outside quotes", () => {
|
||||
// In shell, \; is a literal semicolon and should not split commands.
|
||||
// This should be treated as a single curl command with the URL "https://yaak.app?a=1;b=2"
|
||||
expect(
|
||||
convertCurl('curl https://yaak.app?a=1\\;b=2'),
|
||||
).toEqual({
|
||||
expect(convertCurl("curl https://yaak.app?a=1\\;b=2")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'a', value: '1;b=2', enabled: true },
|
||||
],
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [{ name: "a", value: "1;b=2", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multipart form data with text-only fields from --data-raw', () => {
|
||||
test("Imports multipart form data with text-only fields from --data-raw", () => {
|
||||
const curlCommand = `curl 'http://example.com/api' \
|
||||
-H 'Content-Type: multipart/form-data; boundary=----FormBoundary123' \
|
||||
--data-raw $'------FormBoundary123\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n------FormBoundary123\r\nContent-Disposition: form-data; name="field2"\r\n\r\nvalue2\r\n------FormBoundary123--\r\n'`;
|
||||
@@ -629,20 +607,20 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'http://example.com/api',
|
||||
method: 'POST',
|
||||
url: "http://example.com/api",
|
||||
method: "POST",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data; boundary=----FormBoundary123',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data; boundary=----FormBoundary123",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'field1', value: 'value1', enabled: true },
|
||||
{ name: 'field2', value: 'value2', enabled: true },
|
||||
{ name: "field1", value: "value1", enabled: true },
|
||||
{ name: "field2", value: "value2", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -658,17 +636,17 @@ function baseRequest(mergeWith: Partial<HttpRequest>) {
|
||||
idCount.http_request = (idCount.http_request ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`,
|
||||
model: 'http_request',
|
||||
model: "http_request",
|
||||
authentication: {},
|
||||
authenticationType: null,
|
||||
body: {},
|
||||
bodyType: null,
|
||||
folderId: null,
|
||||
headers: [],
|
||||
method: 'GET',
|
||||
name: '',
|
||||
method: "GET",
|
||||
name: "",
|
||||
sortPriority: 0,
|
||||
url: '',
|
||||
url: "",
|
||||
urlParameters: [],
|
||||
workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
...mergeWith,
|
||||
@@ -679,8 +657,8 @@ function baseWorkspace(mergeWith: Partial<Workspace> = {}) {
|
||||
idCount.workspace = (idCount.workspace ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
model: 'workspace',
|
||||
name: 'Curl Import',
|
||||
model: "workspace",
|
||||
name: "Curl Import",
|
||||
...mergeWith,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-insomnia",
|
||||
"displayName": "Insomnia Importer",
|
||||
"description": "Import data from Insomnia",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import data from Insomnia",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export function isJSObject(obj: unknown) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
|
||||
export function isJSString(obj: unknown) {
|
||||
return Object.prototype.toString.call(obj) === '[object String]';
|
||||
return Object.prototype.toString.call(obj) === "[object String]";
|
||||
}
|
||||
|
||||
export function convertId(id: string): string {
|
||||
if (id.startsWith('GENERATE_ID::')) {
|
||||
if (id.startsWith("GENERATE_ID::")) {
|
||||
return id;
|
||||
}
|
||||
return `GENERATE_ID::${id}`;
|
||||
@@ -17,7 +17,7 @@ export function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
@@ -29,14 +29,14 @@ export function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
export function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
if (typeof obj === "string") {
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- Yaak template syntax
|
||||
return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
|
||||
return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") as T;
|
||||
}
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import YAML from 'yaml';
|
||||
import { deleteUndefinedAttrs, isJSObject } from './common';
|
||||
import { convertInsomniaV4 } from './v4';
|
||||
import { convertInsomniaV5 } from './v5';
|
||||
import type { Context, PluginDefinition } from "@yaakapp/api";
|
||||
import YAML from "yaml";
|
||||
import { deleteUndefinedAttrs, isJSObject } from "./common";
|
||||
import { convertInsomniaV4 } from "./v4";
|
||||
import { convertInsomniaV5 } from "./v5";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Insomnia',
|
||||
description: 'Import Insomnia workspaces',
|
||||
name: "Insomnia",
|
||||
description: "Import Insomnia workspaces",
|
||||
async onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertInsomnia(args.text);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* oxlint-disable no-explicit-any */
|
||||
import type { PartialImportResources } from '@yaakapp/api';
|
||||
import { convertId, convertTemplateSyntax, isJSObject } from './common';
|
||||
import type { PartialImportResources } from "@yaakapp/api";
|
||||
import { convertId, convertTemplateSyntax, isJSObject } from "./common";
|
||||
|
||||
export function convertInsomniaV4(parsed: any) {
|
||||
if (!Array.isArray(parsed.resources)) return null;
|
||||
@@ -16,19 +16,19 @@ export function convertInsomniaV4(parsed: any) {
|
||||
|
||||
// Import workspaces
|
||||
const workspacesToImport = parsed.resources.filter(
|
||||
(r: any) => isJSObject(r) && r._type === 'workspace',
|
||||
(r: any) => isJSObject(r) && r._type === "workspace",
|
||||
);
|
||||
for (const w of workspacesToImport) {
|
||||
resources.workspaces.push({
|
||||
id: convertId(w._id),
|
||||
createdAt: w.created ? new Date(w.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: w.updated ? new Date(w.updated).toISOString().replace('Z', '') : undefined,
|
||||
model: 'workspace',
|
||||
createdAt: w.created ? new Date(w.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: w.updated ? new Date(w.updated).toISOString().replace("Z", "") : undefined,
|
||||
model: "workspace",
|
||||
name: w.name,
|
||||
description: w.description || undefined,
|
||||
});
|
||||
const environmentsToImport = parsed.resources.filter(
|
||||
(r: any) => isJSObject(r) && r._type === 'environment',
|
||||
(r: any) => isJSObject(r) && r._type === "environment",
|
||||
);
|
||||
resources.environments.push(
|
||||
...environmentsToImport.map((r: any) => importEnvironment(r, w._id)),
|
||||
@@ -39,12 +39,12 @@ export function convertInsomniaV4(parsed: any) {
|
||||
for (const child of children) {
|
||||
if (!isJSObject(child)) continue;
|
||||
|
||||
if (child._type === 'request_group') {
|
||||
if (child._type === "request_group") {
|
||||
resources.folders.push(importFolder(child, w._id));
|
||||
nextFolder(child._id);
|
||||
} else if (child._type === 'request') {
|
||||
} else if (child._type === "request") {
|
||||
resources.httpRequests.push(importHttpRequest(child, w._id));
|
||||
} else if (child._type === 'grpc_request') {
|
||||
} else if (child._type === "grpc_request") {
|
||||
resources.grpcRequests.push(importGrpcRequest(child, w._id));
|
||||
}
|
||||
}
|
||||
@@ -63,48 +63,48 @@ export function convertInsomniaV4(parsed: any) {
|
||||
return { resources: convertTemplateSyntax(resources) };
|
||||
}
|
||||
|
||||
function importHttpRequest(r: any, workspaceId: string): PartialImportResources['httpRequests'][0] {
|
||||
function importHttpRequest(r: any, workspaceId: string): PartialImportResources["httpRequests"][0] {
|
||||
let bodyType: string | null = null;
|
||||
let body = {};
|
||||
if (r.body.mimeType === 'application/octet-stream') {
|
||||
bodyType = 'binary';
|
||||
body = { filePath: r.body.fileName ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
|
||||
bodyType = 'application/x-www-form-urlencoded';
|
||||
if (r.body.mimeType === "application/octet-stream") {
|
||||
bodyType = "binary";
|
||||
body = { filePath: r.body.fileName ?? "" };
|
||||
} else if (r.body?.mimeType === "application/x-www-form-urlencoded") {
|
||||
bodyType = "application/x-www-form-urlencoded";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'multipart/form-data') {
|
||||
bodyType = 'multipart/form-data';
|
||||
} else if (r.body?.mimeType === "multipart/form-data") {
|
||||
bodyType = "multipart/form-data";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
file: p.fileName ?? null,
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'application/graphql') {
|
||||
bodyType = 'graphql';
|
||||
body = { text: r.body.text ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/json') {
|
||||
bodyType = 'application/json';
|
||||
body = { text: r.body.text ?? '' };
|
||||
} else if (r.body?.mimeType === "application/graphql") {
|
||||
bodyType = "graphql";
|
||||
body = { text: r.body.text ?? "" };
|
||||
} else if (r.body?.mimeType === "application/json") {
|
||||
bodyType = "application/json";
|
||||
body = { text: r.body.text ?? "" };
|
||||
}
|
||||
|
||||
let authenticationType: string | null = null;
|
||||
let authentication = {};
|
||||
if (r.authentication.type === 'bearer') {
|
||||
authenticationType = 'bearer';
|
||||
if (r.authentication.type === "bearer") {
|
||||
authenticationType = "bearer";
|
||||
authentication = {
|
||||
token: r.authentication.token,
|
||||
};
|
||||
} else if (r.authentication.type === 'basic') {
|
||||
authenticationType = 'basic';
|
||||
} else if (r.authentication.type === "basic") {
|
||||
authenticationType = "basic";
|
||||
authentication = {
|
||||
username: r.authentication.username,
|
||||
password: r.authentication.password,
|
||||
@@ -113,19 +113,19 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
|
||||
|
||||
return {
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: 'http_request',
|
||||
model: "http_request",
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: r.url,
|
||||
urlParameters: (r.parameters ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
})),
|
||||
body,
|
||||
bodyType,
|
||||
@@ -135,51 +135,51 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
|
||||
headers: (r.headers ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? "",
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
.filter(({ name, value }: any) => name !== "" || value !== ""),
|
||||
};
|
||||
}
|
||||
|
||||
function importGrpcRequest(r: any, workspaceId: string): PartialImportResources['grpcRequests'][0] {
|
||||
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
|
||||
function importGrpcRequest(r: any, workspaceId: string): PartialImportResources["grpcRequests"][0] {
|
||||
const parts = r.protoMethodName.split("/").filter((p: any) => p !== "");
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
|
||||
return {
|
||||
id: convertId(r.meta?.id ?? r._id),
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
|
||||
model: 'grpc_request',
|
||||
model: "grpc_request",
|
||||
sortPriority: r.metaSortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: r.url,
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? '',
|
||||
message: r.body?.text ?? "",
|
||||
metadata: (r.metadata ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? "",
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
.filter(({ name, value }: any) => name !== "" || value !== ""),
|
||||
};
|
||||
}
|
||||
|
||||
function importFolder(f: any, workspaceId: string): PartialImportResources['folders'][0] {
|
||||
function importFolder(f: any, workspaceId: string): PartialImportResources["folders"][0] {
|
||||
return {
|
||||
id: convertId(f._id),
|
||||
createdAt: f.created ? new Date(f.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: f.modified ? new Date(f.modified).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: f.created ? new Date(f.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: f.modified ? new Date(f.modified).toISOString().replace("Z", "") : undefined,
|
||||
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
|
||||
workspaceId: convertId(workspaceId),
|
||||
description: f.description || undefined,
|
||||
model: 'folder',
|
||||
model: "folder",
|
||||
name: f.name,
|
||||
};
|
||||
}
|
||||
@@ -188,17 +188,17 @@ function importEnvironment(
|
||||
e: any,
|
||||
workspaceId: string,
|
||||
isParentOg?: boolean,
|
||||
): PartialImportResources['environments'][0] {
|
||||
): PartialImportResources["environments"][0] {
|
||||
const isParent = isParentOg ?? e.parentId === workspaceId;
|
||||
return {
|
||||
id: convertId(e._id),
|
||||
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: e.created ? new Date(e.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: e.modified ? new Date(e.modified).toISOString().replace("Z", "") : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
sortPriority: e.metaSortKey,
|
||||
parentModel: isParent ? 'workspace' : 'environment',
|
||||
parentModel: isParent ? "workspace" : "environment",
|
||||
parentId: null,
|
||||
model: 'environment',
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* oxlint-disable no-explicit-any */
|
||||
import type { PartialImportResources } from '@yaakapp/api';
|
||||
import { convertId, convertTemplateSyntax, isJSObject } from './common';
|
||||
import type { PartialImportResources } from "@yaakapp/api";
|
||||
import { convertId, convertTemplateSyntax, isJSObject } from "./common";
|
||||
|
||||
export function convertInsomniaV5(parsed: any) {
|
||||
// Assert parsed is object
|
||||
if (parsed == null || typeof parsed !== 'object') {
|
||||
if (parsed == null || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!('collection' in parsed) || !Array.isArray(parsed.collection)) {
|
||||
if (!("collection" in parsed) || !Array.isArray(parsed.collection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ export function convertInsomniaV5(parsed: any) {
|
||||
};
|
||||
|
||||
// Import workspaces
|
||||
const meta = ('meta' in parsed ? parsed.meta : {}) as Record<string, any>;
|
||||
const meta = ("meta" in parsed ? parsed.meta : {}) as Record<string, any>;
|
||||
resources.workspaces.push({
|
||||
id: convertId(meta.id ?? 'collection'),
|
||||
createdAt: meta.created ? new Date(meta.created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace('Z', '') : undefined,
|
||||
model: 'workspace',
|
||||
id: convertId(meta.id ?? "collection"),
|
||||
createdAt: meta.created ? new Date(meta.created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace("Z", "") : undefined,
|
||||
model: "workspace",
|
||||
name: parsed.name,
|
||||
description: meta.description || undefined,
|
||||
...importHeaders(parsed),
|
||||
@@ -76,7 +76,7 @@ function importHttpRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['httpRequests'][0] {
|
||||
): PartialImportResources["httpRequests"][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
@@ -84,51 +84,51 @@ function importHttpRequest(
|
||||
|
||||
let bodyType: string | null = null;
|
||||
let body = {};
|
||||
if (r.body?.mimeType === 'application/octet-stream') {
|
||||
bodyType = 'binary';
|
||||
body = { filePath: r.body.fileName ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
|
||||
bodyType = 'application/x-www-form-urlencoded';
|
||||
if (r.body?.mimeType === "application/octet-stream") {
|
||||
bodyType = "binary";
|
||||
body = { filePath: r.body.fileName ?? "" };
|
||||
} else if (r.body?.mimeType === "application/x-www-form-urlencoded") {
|
||||
bodyType = "application/x-www-form-urlencoded";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'multipart/form-data') {
|
||||
bodyType = 'multipart/form-data';
|
||||
} else if (r.body?.mimeType === "multipart/form-data") {
|
||||
bodyType = "multipart/form-data";
|
||||
body = {
|
||||
form: (r.body.params ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
file: p.fileName ?? null,
|
||||
})),
|
||||
};
|
||||
} else if (r.body?.mimeType === 'application/graphql') {
|
||||
bodyType = 'graphql';
|
||||
body = { text: r.body.text ?? '' };
|
||||
} else if (r.body?.mimeType === 'application/json') {
|
||||
bodyType = 'application/json';
|
||||
body = { text: r.body.text ?? '' };
|
||||
} else if (r.body?.mimeType === "application/graphql") {
|
||||
bodyType = "graphql";
|
||||
body = { text: r.body.text ?? "" };
|
||||
} else if (r.body?.mimeType === "application/json") {
|
||||
bodyType = "application/json";
|
||||
body = { text: r.body.text ?? "" };
|
||||
}
|
||||
|
||||
return {
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
model: 'http_request',
|
||||
model: "http_request",
|
||||
name: r.name,
|
||||
description: r.meta?.description || undefined,
|
||||
url: r.url,
|
||||
urlParameters: (r.parameters ?? []).map((p: any) => ({
|
||||
enabled: !p.disabled,
|
||||
name: p.name ?? '',
|
||||
value: p.value ?? '',
|
||||
name: p.name ?? "",
|
||||
value: p.value ?? "",
|
||||
})),
|
||||
body,
|
||||
bodyType,
|
||||
@@ -142,22 +142,22 @@ function importGrpcRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['grpcRequests'][0] {
|
||||
): PartialImportResources["grpcRequests"][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
|
||||
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
|
||||
const parts = r.protoMethodName.split("/").filter((p: any) => p !== "");
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
|
||||
return {
|
||||
model: 'grpc_request',
|
||||
model: "grpc_request",
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
@@ -165,14 +165,14 @@ function importGrpcRequest(
|
||||
url: r.url,
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? '',
|
||||
message: r.body?.text ?? "",
|
||||
metadata: (r.metadata ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? "",
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== ''),
|
||||
.filter(({ name, value }: any) => name !== "" || value !== ""),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,24 +180,24 @@ function importWebsocketRequest(
|
||||
r: any,
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): PartialImportResources['websocketRequests'][0] {
|
||||
): PartialImportResources["websocketRequests"][0] {
|
||||
const id = r.meta?.id ?? r._id;
|
||||
const created = r.meta?.created ?? r.created;
|
||||
const updated = r.meta?.modified ?? r.updated;
|
||||
const sortKey = r.meta?.sortKey ?? r.sortKey;
|
||||
|
||||
return {
|
||||
model: 'websocket_request',
|
||||
model: "websocket_request",
|
||||
id: convertId(id),
|
||||
workspaceId: convertId(workspaceId),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
name: r.name,
|
||||
description: r.description || undefined,
|
||||
url: r.url,
|
||||
message: r.body?.text ?? '',
|
||||
message: r.body?.text ?? "",
|
||||
...importHeaders(r),
|
||||
...importAuthentication(r),
|
||||
};
|
||||
@@ -207,23 +207,23 @@ function importHeaders(obj: any) {
|
||||
const headers = (obj.headers ?? [])
|
||||
.map((h: any) => ({
|
||||
enabled: !h.disabled,
|
||||
name: h.name ?? '',
|
||||
value: h.value ?? '',
|
||||
name: h.name ?? "",
|
||||
value: h.value ?? "",
|
||||
}))
|
||||
.filter(({ name, value }: any) => name !== '' || value !== '');
|
||||
.filter(({ name, value }: any) => name !== "" || value !== "");
|
||||
return { headers } as const;
|
||||
}
|
||||
|
||||
function importAuthentication(obj: any) {
|
||||
let authenticationType: string | null = null;
|
||||
let authentication = {};
|
||||
if (obj.authentication?.type === 'bearer') {
|
||||
authenticationType = 'bearer';
|
||||
if (obj.authentication?.type === "bearer") {
|
||||
authenticationType = "bearer";
|
||||
authentication = {
|
||||
token: obj.authentication.token,
|
||||
};
|
||||
} else if (obj.authentication?.type === 'basic') {
|
||||
authenticationType = 'basic';
|
||||
} else if (obj.authentication?.type === "basic") {
|
||||
authenticationType = "basic";
|
||||
authentication = {
|
||||
username: obj.authentication.username,
|
||||
password: obj.authentication.password,
|
||||
@@ -238,26 +238,26 @@ function importFolder(
|
||||
workspaceId: string,
|
||||
parentId: string,
|
||||
): {
|
||||
folder: PartialImportResources['folders'][0];
|
||||
environment: PartialImportResources['environments'][0] | null;
|
||||
folder: PartialImportResources["folders"][0];
|
||||
environment: PartialImportResources["environments"][0] | null;
|
||||
} {
|
||||
const id = f.meta?.id ?? f._id;
|
||||
const created = f.meta?.created ?? f.created;
|
||||
const updated = f.meta?.modified ?? f.updated;
|
||||
const sortKey = f.meta?.sortKey ?? f.sortKey;
|
||||
|
||||
let environment: PartialImportResources['environments'][0] | null = null;
|
||||
let environment: PartialImportResources["environments"][0] | null = null;
|
||||
if (Object.keys(f.environment ?? {}).length > 0) {
|
||||
environment = {
|
||||
id: convertId(`${id}folder`),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
public: true,
|
||||
parentModel: 'folder',
|
||||
parentModel: "folder",
|
||||
parentId: convertId(id),
|
||||
model: 'environment',
|
||||
name: 'Folder Environment',
|
||||
model: "environment",
|
||||
name: "Folder Environment",
|
||||
variables: Object.entries(f.environment ?? {}).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
@@ -268,10 +268,10 @@ function importFolder(
|
||||
|
||||
return {
|
||||
folder: {
|
||||
model: 'folder',
|
||||
model: "folder",
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
folderId: parentId === workspaceId ? null : convertId(parentId),
|
||||
sortPriority: sortKey,
|
||||
workspaceId: convertId(workspaceId),
|
||||
@@ -288,7 +288,7 @@ function importEnvironment(
|
||||
e: any,
|
||||
workspaceId: string,
|
||||
isParent?: boolean,
|
||||
): PartialImportResources['environments'][0] {
|
||||
): PartialImportResources["environments"][0] {
|
||||
const id = e.meta?.id ?? e._id;
|
||||
const created = e.meta?.created ?? e.created;
|
||||
const updated = e.meta?.modified ?? e.updated;
|
||||
@@ -296,14 +296,14 @@ function importEnvironment(
|
||||
|
||||
return {
|
||||
id: convertId(id),
|
||||
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
|
||||
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
|
||||
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
|
||||
workspaceId: convertId(workspaceId),
|
||||
public: !e.isPrivate,
|
||||
sortPriority: sortKey,
|
||||
parentModel: isParent ? 'workspace' : 'environment',
|
||||
parentModel: isParent ? "workspace" : "environment",
|
||||
parentId: null,
|
||||
model: 'environment',
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import YAML from 'yaml';
|
||||
import { convertInsomnia } from '../src';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import YAML from "yaml";
|
||||
import { convertInsomnia } from "../src";
|
||||
|
||||
describe('importer-yaak', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
describe("importer-yaak", () => {
|
||||
const p = path.join(__dirname, "fixtures");
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.includes('.output')) {
|
||||
if (fixture.includes(".output")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test(`Imports ${fixture}`, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const contents = fs.readFileSync(path.join(p, fixture), "utf-8");
|
||||
const expected = fs.readFileSync(
|
||||
path.join(p, fixture.replace(/.input\..*/, '.output.json')),
|
||||
'utf-8',
|
||||
path.join(p, fixture.replace(/.input\..*/, ".output.json")),
|
||||
"utf-8",
|
||||
);
|
||||
const result = convertInsomnia(contents);
|
||||
// console.log(JSON.stringify(result, null, 2))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-openapi",
|
||||
"displayName": "OpenAPI Importer",
|
||||
"description": "Import API specifications from OpenAPI/Swagger format",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import API specifications from OpenAPI/Swagger format",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { convertPostman } from '@yaak/importer-postman/src';
|
||||
import type { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
|
||||
import { convert } from 'openapi-to-postmanv2';
|
||||
import { convertPostman } from "@yaak/importer-postman/src";
|
||||
import type { Context, PluginDefinition } from "@yaakapp/api";
|
||||
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
||||
import { convert } from "openapi-to-postmanv2";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'OpenAPI',
|
||||
description: 'Import OpenAPI collections',
|
||||
name: "OpenAPI",
|
||||
description: "Import OpenAPI collections",
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertOpenApi(args.text);
|
||||
},
|
||||
@@ -19,7 +19,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
||||
try {
|
||||
postmanCollection = await new Promise((resolve, reject) => {
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
convert({ type: 'string', data: contents }, {}, (err, result: any) => {
|
||||
convert({ type: "string", data: contents }, {}, (err, result: any) => {
|
||||
if (err != null) reject(err);
|
||||
|
||||
if (Array.isArray(result.output) && result.output.length > 0) {
|
||||
|
||||
@@ -13,23 +13,23 @@ info:
|
||||
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
|
||||
version: 1.0.20-SNAPSHOT
|
||||
title: Swagger Petstore - OpenAPI 3.0
|
||||
termsOfService: 'http://swagger.io/terms/'
|
||||
termsOfService: "http://swagger.io/terms/"
|
||||
contact:
|
||||
email: apiteam@swagger.io
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
tags:
|
||||
- name: pet
|
||||
description: Everything about your Pets
|
||||
externalDocs:
|
||||
description: Find out more
|
||||
url: 'http://swagger.io'
|
||||
url: "http://swagger.io"
|
||||
- name: store
|
||||
description: Access to Petstore orders
|
||||
externalDocs:
|
||||
description: Find out more about our store
|
||||
url: 'http://swagger.io'
|
||||
url: "http://swagger.io"
|
||||
- name: user
|
||||
description: Operations about user
|
||||
paths:
|
||||
@@ -41,34 +41,34 @@ paths:
|
||||
description: Add a new pet to the store
|
||||
operationId: addPet
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'405':
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"405":
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
requestBody:
|
||||
description: Create a new pet in the store
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
put:
|
||||
tags:
|
||||
- pet
|
||||
@@ -76,38 +76,38 @@ paths:
|
||||
description: Update an existing pet by Id
|
||||
operationId: updatePet
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"400":
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
"404":
|
||||
description: Pet not found
|
||||
'405':
|
||||
"405":
|
||||
description: Validation exception
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
requestBody:
|
||||
description: Update an existent pet in the store
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
/pet/findByStatus:
|
||||
get:
|
||||
tags:
|
||||
@@ -129,25 +129,25 @@ paths:
|
||||
- sold
|
||||
default: available
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"400":
|
||||
description: Invalid status value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
/pet/findByTags:
|
||||
get:
|
||||
tags:
|
||||
@@ -168,26 +168,26 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"400":
|
||||
description: Invalid tag value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
'/pet/{petId}':
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
"/pet/{petId}":
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
@@ -203,29 +203,29 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
'400':
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"400":
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
"404":
|
||||
description: Pet not found
|
||||
security:
|
||||
- api_key: []
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Updates a pet in the store with form data
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: updatePetWithForm
|
||||
parameters:
|
||||
- name: petId
|
||||
@@ -246,22 +246,22 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'405':
|
||||
"405":
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
delete:
|
||||
tags:
|
||||
- pet
|
||||
summary: Deletes a pet
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: deletePet
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: header
|
||||
description: ''
|
||||
description: ""
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -273,18 +273,18 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
"400":
|
||||
description: Invalid pet value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
'/pet/{petId}/uploadImage':
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
"/pet/{petId}/uploadImage":
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: uploads an image
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: uploadFile
|
||||
parameters:
|
||||
- name: petId
|
||||
@@ -301,16 +301,16 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: "#/components/schemas/ApiResponse"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- 'write:pets'
|
||||
- 'read:pets'
|
||||
- "write:pets"
|
||||
- "read:pets"
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream:
|
||||
@@ -326,7 +326,7 @@ paths:
|
||||
operationId: getInventory
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
@@ -346,26 +346,26 @@ paths:
|
||||
operationId: placeOrder
|
||||
x-swagger-router-controller: OrderController
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'405':
|
||||
$ref: "#/components/schemas/Order"
|
||||
"405":
|
||||
description: Invalid input
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
$ref: "#/components/schemas/Order"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
$ref: "#/components/schemas/Order"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'/store/order/{orderId}':
|
||||
$ref: "#/components/schemas/Order"
|
||||
"/store/order/{orderId}":
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
@@ -384,18 +384,18 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
$ref: "#/components/schemas/Order"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'400':
|
||||
$ref: "#/components/schemas/Order"
|
||||
"400":
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
"404":
|
||||
description: Order not found
|
||||
delete:
|
||||
tags:
|
||||
@@ -415,9 +415,9 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
"400":
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
"404":
|
||||
description: Order not found
|
||||
/user:
|
||||
post:
|
||||
@@ -432,40 +432,40 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
description: Created user object
|
||||
/user/createWithList:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Creates list of users with given input array
|
||||
description: 'Creates list of users with given input array'
|
||||
description: "Creates list of users with given input array"
|
||||
x-swagger-router-controller: UserController
|
||||
operationId: createUsersWithListInput
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
default:
|
||||
description: successful operation
|
||||
requestBody:
|
||||
@@ -474,13 +474,13 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
/user/login:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs user into the system
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: loginUser
|
||||
parameters:
|
||||
- name: username
|
||||
@@ -496,7 +496,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
headers:
|
||||
X-Rate-Limit:
|
||||
@@ -516,46 +516,46 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
"400":
|
||||
description: Invalid username/password supplied
|
||||
/user/logout:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs out current logged in user session
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: logoutUser
|
||||
parameters: []
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
'/user/{username}':
|
||||
"/user/{username}":
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Get user by user name
|
||||
description: ''
|
||||
description: ""
|
||||
operationId: getUserByName
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: 'The name that needs to be fetched. Use user1 for testing. '
|
||||
description: "The name that needs to be fetched. Use user1 for testing. "
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
$ref: "#/components/schemas/User"
|
||||
"400":
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
"404":
|
||||
description: User not found
|
||||
put:
|
||||
tags:
|
||||
@@ -579,13 +579,13 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
delete:
|
||||
tags:
|
||||
- user
|
||||
@@ -600,13 +600,13 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'400':
|
||||
"400":
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
"404":
|
||||
description: User not found
|
||||
externalDocs:
|
||||
description: Find out more about Swagger
|
||||
url: 'http://swagger.io'
|
||||
url: "http://swagger.io"
|
||||
components:
|
||||
schemas:
|
||||
Order:
|
||||
@@ -652,7 +652,7 @@ components:
|
||||
address:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Address'
|
||||
$ref: "#/components/schemas/Address"
|
||||
xml:
|
||||
wrapped: true
|
||||
name: addresses
|
||||
@@ -747,7 +747,7 @@ components:
|
||||
type: string
|
||||
example: doggie
|
||||
category:
|
||||
$ref: '#/components/schemas/Category'
|
||||
$ref: "#/components/schemas/Category"
|
||||
photoUrls:
|
||||
type: array
|
||||
xml:
|
||||
@@ -761,7 +761,7 @@ components:
|
||||
xml:
|
||||
wrapped: true
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
$ref: "#/components/schemas/Tag"
|
||||
xml:
|
||||
name: tag
|
||||
status:
|
||||
@@ -784,17 +784,17 @@ components:
|
||||
message:
|
||||
type: string
|
||||
xml:
|
||||
name: '##default'
|
||||
name: "##default"
|
||||
type: object
|
||||
requestBodies:
|
||||
Pet:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
$ref: "#/components/schemas/Pet"
|
||||
description: Pet object that needs to be added to the store
|
||||
UserArray:
|
||||
content:
|
||||
@@ -802,17 +802,17 @@ components:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: "#/components/schemas/User"
|
||||
description: List of user object
|
||||
securitySchemes:
|
||||
petstore_auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
implicit:
|
||||
authorizationUrl: 'https://petstore.swagger.io/oauth/authorize'
|
||||
authorizationUrl: "https://petstore.swagger.io/oauth/authorize"
|
||||
scopes:
|
||||
'write:pets': modify pets in your account
|
||||
'read:pets': read your pets
|
||||
"write:pets": modify pets in your account
|
||||
"read:pets": read your pets
|
||||
api_key:
|
||||
type: apiKey
|
||||
name: api_key
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convertOpenApi } from '../src';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertOpenApi } from "../src";
|
||||
|
||||
describe('importer-openapi', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
describe("importer-openapi", () => {
|
||||
const p = path.join(__dirname, "fixtures");
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
test('Maps operation description to request description', async () => {
|
||||
test("Maps operation description to request description", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: '3.0.0',
|
||||
info: { title: 'Description Test', version: '1.0.0' },
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Description Test", version: "1.0.0" },
|
||||
paths: {
|
||||
'/klanten': {
|
||||
"/klanten": {
|
||||
get: {
|
||||
description: 'Lijst van klanten',
|
||||
responses: { '200': { description: 'ok' } },
|
||||
description: "Lijst van klanten",
|
||||
responses: { "200": { description: "ok" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -25,24 +25,24 @@ describe('importer-openapi', () => {
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
description: 'Lijst van klanten',
|
||||
description: "Lijst van klanten",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Skips invalid file', async () => {
|
||||
const imported = await convertOpenApi('{}');
|
||||
test("Skips invalid file", async () => {
|
||||
const imported = await convertOpenApi("{}");
|
||||
expect(imported).toBeUndefined();
|
||||
});
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
test(`Imports ${fixture}`, async () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const contents = fs.readFileSync(path.join(p, fixture), "utf-8");
|
||||
const imported = await convertOpenApi(contents);
|
||||
expect(imported?.resources.workspaces).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Swagger Petstore - OpenAPI 3.0',
|
||||
description: expect.stringContaining('This is a sample Pet Store Server'),
|
||||
name: "Swagger Petstore - OpenAPI 3.0",
|
||||
description: expect.stringContaining("This is a sample Pet Store Server"),
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests.length).toBe(19);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-postman-environment",
|
||||
"displayName": "Postman Environment Importer",
|
||||
"description": "Import environments from Postman",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import environments from Postman",
|
||||
"main": "./build/index.js",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
|
||||
@@ -5,20 +5,20 @@ import type {
|
||||
PartialImportResources,
|
||||
PluginDefinition,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
|
||||
} from "@yaakapp/api";
|
||||
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
||||
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId">[];
|
||||
}
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Postman Environment',
|
||||
description: 'Import postman environment exports',
|
||||
name: "Postman Environment",
|
||||
description: "Import postman environment exports",
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertPostmanEnvironment(args.text);
|
||||
},
|
||||
@@ -38,9 +38,9 @@ export function convertPostmanEnvironment(contents: string): ImportPluginRespons
|
||||
type?: string;
|
||||
}>(root.values);
|
||||
const scope = root._postman_variable_scope;
|
||||
const hasEnvMarkers = typeof scope === 'string';
|
||||
const hasEnvMarkers = typeof scope === "string";
|
||||
|
||||
if (values.length === 0 || (!hasEnvMarkers && typeof root.name !== 'string')) {
|
||||
if (values.length === 0 || (!hasEnvMarkers && typeof root.name !== "string")) {
|
||||
// Not a Postman environment file, skip
|
||||
return;
|
||||
}
|
||||
@@ -53,18 +53,18 @@ export function convertPostmanEnvironment(contents: string): ImportPluginRespons
|
||||
const envVariables = values
|
||||
.map((v) => ({
|
||||
enabled: v.enabled ?? true,
|
||||
name: String(v.key ?? ''),
|
||||
name: String(v.key ?? ""),
|
||||
value: String(v.value),
|
||||
description: v.description ? String(v.description) : null,
|
||||
}))
|
||||
.filter((v) => v.name.length > 0);
|
||||
|
||||
const environment: ExportResources['environments'][0] = {
|
||||
model: 'environment',
|
||||
id: generateId('environment'),
|
||||
name: root.name ? String(root.name) : 'Environment',
|
||||
workspaceId: 'CURRENT_WORKSPACE',
|
||||
parentModel: 'environment',
|
||||
const environment: ExportResources["environments"][0] = {
|
||||
model: "environment",
|
||||
id: generateId("environment"),
|
||||
name: root.name ? String(root.name) : "Environment",
|
||||
workspaceId: "CURRENT_WORKSPACE",
|
||||
parentModel: "environment",
|
||||
parentId: null,
|
||||
variables: envVariables,
|
||||
};
|
||||
@@ -86,26 +86,26 @@ function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
|
||||
}
|
||||
|
||||
function toRecord<T>(value: unknown): Record<string, T> {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
return value as Record<string, T>;
|
||||
}
|
||||
return {} as Record<string, T>;
|
||||
}
|
||||
|
||||
function toArray<T>(value: unknown): T[] {
|
||||
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
|
||||
if (Object.prototype.toString.call(value) === "[object Array]") return value as T[];
|
||||
return [] as T[];
|
||||
}
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
if (typeof obj === "string") {
|
||||
return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => `\${[${expr.trim()}]}`) as T;
|
||||
}
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
@@ -117,7 +117,7 @@ function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convertPostmanEnvironment } from '../src';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertPostmanEnvironment } from "../src";
|
||||
|
||||
describe('importer-postman-environment', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
describe("importer-postman-environment", () => {
|
||||
const p = path.join(__dirname, "fixtures");
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.includes('.output')) {
|
||||
if (fixture.includes(".output")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test(`Imports ${fixture}`, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
|
||||
const contents = fs.readFileSync(path.join(p, fixture), "utf-8");
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace(".input", ".output")), "utf-8");
|
||||
const result = convertPostmanEnvironment(contents);
|
||||
expect(result).toEqual(JSON.parse(expected));
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-postman",
|
||||
"displayName": "Postman Importer",
|
||||
"description": "Import collections from Postman",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import collections from Postman",
|
||||
"main": "./build/index.js",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
|
||||
@@ -9,26 +9,26 @@ import type {
|
||||
PartialImportResources,
|
||||
PluginDefinition,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
|
||||
} from "@yaakapp/api";
|
||||
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
||||
|
||||
const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
|
||||
const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
|
||||
const POSTMAN_2_1_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
|
||||
const POSTMAN_2_0_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json";
|
||||
const VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
||||
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId">[];
|
||||
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
||||
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
||||
}
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Postman',
|
||||
description: 'Import postman collections',
|
||||
name: "Postman",
|
||||
description: "Import postman collections",
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
return convertPostman(args.text);
|
||||
},
|
||||
@@ -41,7 +41,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
|
||||
const info = toRecord(root.info);
|
||||
const isValidSchema = VALID_SCHEMAS.includes(
|
||||
typeof info.schema === 'string' ? info.schema : 'n/a',
|
||||
typeof info.schema === "string" ? info.schema : "n/a",
|
||||
);
|
||||
if (!isValidSchema || !Array.isArray(root.item)) {
|
||||
return;
|
||||
@@ -56,22 +56,22 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
folders: [],
|
||||
};
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: generateId('workspace'),
|
||||
name: info.name ? String(info.name) : 'Postman Import',
|
||||
const workspace: ExportResources["workspaces"][0] = {
|
||||
model: "workspace",
|
||||
id: generateId("workspace"),
|
||||
name: info.name ? String(info.name) : "Postman Import",
|
||||
description: importDescription(info.description),
|
||||
...globalAuth,
|
||||
};
|
||||
exportResources.workspaces.push(workspace);
|
||||
|
||||
// Create the base environment
|
||||
const environment: ExportResources['environments'][0] = {
|
||||
model: 'environment',
|
||||
id: generateId('environment'),
|
||||
name: 'Global Variables',
|
||||
const environment: ExportResources["environments"][0] = {
|
||||
model: "environment",
|
||||
id: generateId("environment"),
|
||||
name: "Global Variables",
|
||||
workspaceId: workspace.id,
|
||||
parentModel: 'workspace',
|
||||
parentModel: "workspace",
|
||||
parentId: null,
|
||||
variables:
|
||||
toArray<{ key: string; value: string }>(root.variable).map((v) => ({
|
||||
@@ -83,12 +83,12 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
|
||||
let sortPriorityIndex = 0;
|
||||
const importItem = (v: Record<string, unknown>, folderId: string | null = null) => {
|
||||
if (typeof v.name === 'string' && Array.isArray(v.item)) {
|
||||
const folder: ExportResources['folders'][0] = {
|
||||
model: 'folder',
|
||||
if (typeof v.name === "string" && Array.isArray(v.item)) {
|
||||
const folder: ExportResources["folders"][0] = {
|
||||
model: "folder",
|
||||
sortPriority: sortPriorityIndex++,
|
||||
workspaceId: workspace.id,
|
||||
id: generateId('folder'),
|
||||
id: generateId("folder"),
|
||||
name: v.name,
|
||||
folderId,
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
for (const child of v.item) {
|
||||
importItem(child, folder.id);
|
||||
}
|
||||
} else if (typeof v.name === 'string' && 'request' in v) {
|
||||
} else if (typeof v.name === "string" && "request" in v) {
|
||||
const r = toRecord(v.request);
|
||||
const bodyPatch = importBody(r.body);
|
||||
const requestAuth = importAuth(r.auth);
|
||||
@@ -126,14 +126,14 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
|
||||
const { url, urlParameters } = convertUrl(r.url);
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
model: 'http_request',
|
||||
id: generateId('http_request'),
|
||||
const request: ExportResources["httpRequests"][0] = {
|
||||
model: "http_request",
|
||||
id: generateId("http_request"),
|
||||
workspaceId: workspace.id,
|
||||
folderId,
|
||||
name: v.name,
|
||||
description: importDescription(r.description),
|
||||
method: typeof r.method === 'string' ? r.method : 'GET',
|
||||
method: typeof r.method === "string" ? r.method : "GET",
|
||||
url,
|
||||
urlParameters,
|
||||
body: bodyPatch.body,
|
||||
@@ -144,7 +144,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
};
|
||||
exportResources.httpRequests.push(request);
|
||||
} else {
|
||||
console.log('Unknown item', v, folderId);
|
||||
console.log("Unknown item", v, folderId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,53 +159,53 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
|
||||
return { resources };
|
||||
}
|
||||
|
||||
function convertUrl(rawUrl: unknown): Pick<HttpRequest, 'url' | 'urlParameters'> {
|
||||
if (typeof rawUrl === 'string') {
|
||||
function convertUrl(rawUrl: unknown): Pick<HttpRequest, "url" | "urlParameters"> {
|
||||
if (typeof rawUrl === "string") {
|
||||
return { url: rawUrl, urlParameters: [] };
|
||||
}
|
||||
|
||||
const url = toRecord(rawUrl);
|
||||
|
||||
let v = '';
|
||||
let v = "";
|
||||
|
||||
if ('protocol' in url && typeof url.protocol === 'string') {
|
||||
if ("protocol" in url && typeof url.protocol === "string") {
|
||||
v += `${url.protocol}://`;
|
||||
}
|
||||
|
||||
if ('host' in url) {
|
||||
v += `${Array.isArray(url.host) ? url.host.join('.') : String(url.host)}`;
|
||||
if ("host" in url) {
|
||||
v += `${Array.isArray(url.host) ? url.host.join(".") : String(url.host)}`;
|
||||
}
|
||||
|
||||
if ('port' in url && typeof url.port === 'string') {
|
||||
if ("port" in url && typeof url.port === "string") {
|
||||
v += `:${url.port}`;
|
||||
}
|
||||
|
||||
if ('path' in url && Array.isArray(url.path) && url.path.length > 0) {
|
||||
v += `/${Array.isArray(url.path) ? url.path.join('/') : url.path}`;
|
||||
if ("path" in url && Array.isArray(url.path) && url.path.length > 0) {
|
||||
v += `/${Array.isArray(url.path) ? url.path.join("/") : url.path}`;
|
||||
}
|
||||
|
||||
const params: HttpUrlParameter[] = [];
|
||||
if ('query' in url && Array.isArray(url.query) && url.query.length > 0) {
|
||||
if ("query" in url && Array.isArray(url.query) && url.query.length > 0) {
|
||||
for (const query of url.query) {
|
||||
params.push({
|
||||
name: query.key ?? '',
|
||||
value: query.value ?? '',
|
||||
name: query.key ?? "",
|
||||
value: query.value ?? "",
|
||||
enabled: !query.disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('variable' in url && Array.isArray(url.variable) && url.variable.length > 0) {
|
||||
if ("variable" in url && Array.isArray(url.variable) && url.variable.length > 0) {
|
||||
for (const v of url.variable) {
|
||||
params.push({
|
||||
name: `:${v.key ?? ''}`,
|
||||
value: v.value ?? '',
|
||||
name: `:${v.key ?? ""}`,
|
||||
value: v.value ?? "",
|
||||
enabled: !v.disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('hash' in url && typeof url.hash === 'string') {
|
||||
if ("hash" in url && typeof url.hash === "string") {
|
||||
v += `#${url.hash}`;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ function convertUrl(rawUrl: unknown): Pick<HttpRequest, 'url' | 'urlParameters'>
|
||||
return { url: v, urlParameters: params };
|
||||
}
|
||||
|
||||
function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
|
||||
function importAuth(rawAuth: unknown): Pick<HttpRequest, "authentication" | "authenticationType"> {
|
||||
const auth = toRecord<Record<string, string>>(rawAuth);
|
||||
|
||||
// Helper: Postman stores auth params as an array of { key, value, ... }
|
||||
@@ -223,7 +223,7 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
const o: Record<string, unknown> = {};
|
||||
for (const i of v) {
|
||||
const ii = toRecord(i);
|
||||
if (typeof ii.key === 'string') {
|
||||
if (typeof ii.key === "string") {
|
||||
o[ii.key] = ii.value;
|
||||
}
|
||||
}
|
||||
@@ -232,39 +232,39 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
|
||||
const authType: string | undefined = auth.type ? String(auth.type) : undefined;
|
||||
|
||||
if (authType === 'noauth') {
|
||||
if (authType === "noauth") {
|
||||
return {
|
||||
authenticationType: 'none',
|
||||
authenticationType: "none",
|
||||
authentication: {},
|
||||
};
|
||||
}
|
||||
|
||||
if ('basic' in auth && authType === 'basic') {
|
||||
if ("basic" in auth && authType === "basic") {
|
||||
const b = pmArrayToObj(auth.basic);
|
||||
return {
|
||||
authenticationType: 'basic',
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: String(b.username ?? ''),
|
||||
password: String(b.password ?? ''),
|
||||
username: String(b.username ?? ""),
|
||||
password: String(b.password ?? ""),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ('bearer' in auth && authType === 'bearer') {
|
||||
if ("bearer" in auth && authType === "bearer") {
|
||||
const b = pmArrayToObj(auth.bearer);
|
||||
// Postman uses key "token"
|
||||
return {
|
||||
authenticationType: 'bearer',
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: String(b.token ?? ''),
|
||||
token: String(b.token ?? ""),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ('awsv4' in auth && authType === 'awsv4') {
|
||||
if ("awsv4" in auth && authType === "awsv4") {
|
||||
const a = pmArrayToObj(auth.awsv4);
|
||||
return {
|
||||
authenticationType: 'awsv4',
|
||||
authenticationType: "awsv4",
|
||||
authentication: {
|
||||
accessKeyId: a.accessKey != null ? String(a.accessKey) : undefined,
|
||||
secretAccessKey: a.secretKey != null ? String(a.secretKey) : undefined,
|
||||
@@ -275,51 +275,51 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
};
|
||||
}
|
||||
|
||||
if ('apikey' in auth && authType === 'apikey') {
|
||||
if ("apikey" in auth && authType === "apikey") {
|
||||
const a = pmArrayToObj(auth.apikey);
|
||||
return {
|
||||
authenticationType: 'apikey',
|
||||
authenticationType: "apikey",
|
||||
authentication: {
|
||||
location: a.in === 'query' ? 'query' : 'header',
|
||||
location: a.in === "query" ? "query" : "header",
|
||||
key: a.value != null ? String(a.value) : undefined,
|
||||
value: a.key != null ? String(a.key) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ('jwt' in auth && authType === 'jwt') {
|
||||
if ("jwt" in auth && authType === "jwt") {
|
||||
const a = pmArrayToObj(auth.jwt);
|
||||
return {
|
||||
authenticationType: 'jwt',
|
||||
authenticationType: "jwt",
|
||||
authentication: {
|
||||
algorithm: a.algorithm != null ? String(a.algorithm).toUpperCase() : undefined,
|
||||
secret: a.secret != null ? String(a.secret) : undefined,
|
||||
secretBase64: !!a.isSecretBase64Encoded,
|
||||
payload: a.payload != null ? String(a.payload) : undefined,
|
||||
headerPrefix: a.headerPrefix != null ? String(a.headerPrefix) : undefined,
|
||||
location: a.addTokenTo === 'header' ? 'header' : 'query',
|
||||
location: a.addTokenTo === "header" ? "header" : "query",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ('oauth2' in auth && authType === 'oauth2') {
|
||||
if ("oauth2" in auth && authType === "oauth2") {
|
||||
const o = pmArrayToObj(auth.oauth2);
|
||||
|
||||
let grantType = o.grant_type ? String(o.grant_type) : 'authorization_code';
|
||||
let grantType = o.grant_type ? String(o.grant_type) : "authorization_code";
|
||||
let pkcePatch: Record<string, unknown> = {};
|
||||
|
||||
if (grantType === 'authorization_code_with_pkce') {
|
||||
grantType = 'authorization_code';
|
||||
if (grantType === "authorization_code_with_pkce") {
|
||||
grantType = "authorization_code";
|
||||
pkcePatch =
|
||||
o.grant_type === 'authorization_code_with_pkce'
|
||||
o.grant_type === "authorization_code_with_pkce"
|
||||
? {
|
||||
usePkce: true,
|
||||
pkceChallengeMethod: o.challengeAlgorithm ?? undefined,
|
||||
pkceCodeVerifier: o.code_verifier != null ? String(o.code_verifier) : undefined,
|
||||
}
|
||||
: {};
|
||||
} else if (grantType === 'password_credentials') {
|
||||
grantType = 'password';
|
||||
} else if (grantType === "password_credentials") {
|
||||
grantType = "password";
|
||||
}
|
||||
|
||||
const accessTokenUrl = o.accessTokenUrl != null ? String(o.accessTokenUrl) : undefined;
|
||||
@@ -327,8 +327,8 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
const authorizationUrl = o.authUrl != null ? String(o.authUrl) : undefined;
|
||||
const clientId = o.clientId != null ? String(o.clientId) : undefined;
|
||||
const clientSecret = o.clientSecret != null ? String(o.clientSecret) : undefined;
|
||||
const credentials = o.client_authentication === 'body' ? 'body' : undefined;
|
||||
const headerPrefix = o.headerPrefix ?? 'Bearer';
|
||||
const credentials = o.client_authentication === "body" ? "body" : undefined;
|
||||
const headerPrefix = o.headerPrefix ?? "Bearer";
|
||||
const password = o.password != null ? String(o.password) : undefined;
|
||||
const redirectUri = o.redirect_uri != null ? String(o.redirect_uri) : undefined;
|
||||
const scope = o.scope != null ? String(o.scope) : undefined;
|
||||
@@ -336,7 +336,7 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
const username = o.username != null ? String(o.username) : undefined;
|
||||
|
||||
let grantPatch: Record<string, unknown> = {};
|
||||
if (grantType === 'authorization_code') {
|
||||
if (grantType === "authorization_code") {
|
||||
grantPatch = {
|
||||
clientSecret,
|
||||
authorizationUrl,
|
||||
@@ -345,16 +345,16 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
state,
|
||||
...pkcePatch,
|
||||
};
|
||||
} else if (grantType === 'implicit') {
|
||||
} else if (grantType === "implicit") {
|
||||
grantPatch = { authorizationUrl, redirectUri, state };
|
||||
} else if (grantType === 'password') {
|
||||
} else if (grantType === "password") {
|
||||
grantPatch = { clientSecret, accessTokenUrl, username, password };
|
||||
} else if (grantType === 'client_credentials') {
|
||||
} else if (grantType === "client_credentials") {
|
||||
grantPatch = { clientSecret, accessTokenUrl };
|
||||
}
|
||||
|
||||
const authentication = {
|
||||
name: 'oauth2',
|
||||
name: "oauth2",
|
||||
grantType,
|
||||
audience,
|
||||
clientId,
|
||||
@@ -364,13 +364,13 @@ function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'aut
|
||||
...grantPatch,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
return { authenticationType: 'oauth2', authentication };
|
||||
return { authenticationType: "oauth2", authentication };
|
||||
}
|
||||
|
||||
return { authenticationType: null, authentication: {} };
|
||||
}
|
||||
|
||||
function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
|
||||
function importBody(rawBody: unknown): Pick<HttpRequest, "body" | "bodyType" | "headers"> {
|
||||
const body = toRecord(rawBody) as {
|
||||
mode: string;
|
||||
graphql: { query?: string; variables?: string };
|
||||
@@ -386,21 +386,21 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
|
||||
options?: { raw?: { language?: string } };
|
||||
file?: { src?: string };
|
||||
};
|
||||
if (body.mode === 'graphql') {
|
||||
if (body.mode === "graphql") {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
name: "Content-Type",
|
||||
value: "application/json",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'graphql',
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{
|
||||
query: body.graphql?.query || '',
|
||||
variables: parseJSONToRecord(body.graphql?.variables || '{}'),
|
||||
query: body.graphql?.query || "",
|
||||
variables: parseJSONToRecord(body.graphql?.variables || "{}"),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -408,72 +408,72 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
|
||||
},
|
||||
};
|
||||
}
|
||||
if (body.mode === 'urlencoded') {
|
||||
if (body.mode === "urlencoded") {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: toArray<NonNullable<typeof body.urlencoded>[0]>(body.urlencoded).map((f) => ({
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
name: f.key ?? "",
|
||||
value: f.value ?? "",
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (body.mode === 'formdata') {
|
||||
if (body.mode === "formdata") {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: toArray<NonNullable<typeof body.formdata>[0]>(body.formdata).map((f) =>
|
||||
f.src != null
|
||||
? {
|
||||
enabled: !f.disabled,
|
||||
contentType: f.contentType ?? null,
|
||||
name: f.key ?? '',
|
||||
file: f.src ?? '',
|
||||
name: f.key ?? "",
|
||||
file: f.src ?? "",
|
||||
}
|
||||
: {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
name: f.key ?? "",
|
||||
value: f.value ?? "",
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (body.mode === 'raw') {
|
||||
if (body.mode === "raw") {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: body.options?.raw?.language === 'json' ? 'application/json' : '',
|
||||
name: "Content-Type",
|
||||
value: body.options?.raw?.language === "json" ? "application/json" : "",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other',
|
||||
bodyType: body.options?.raw?.language === "json" ? "application/json" : "other",
|
||||
body: {
|
||||
text: body.raw ?? '',
|
||||
text: body.raw ?? "",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (body.mode === 'file') {
|
||||
if (body.mode === "file") {
|
||||
return {
|
||||
headers: [],
|
||||
bodyType: 'binary',
|
||||
bodyType: "binary",
|
||||
body: {
|
||||
filePath: body.file?.src,
|
||||
},
|
||||
@@ -491,14 +491,14 @@ function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
|
||||
}
|
||||
|
||||
function toRecord<T>(value: unknown): Record<string, T> {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
return value as Record<string, T>;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function toArray<T>(value: unknown): T[] {
|
||||
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
|
||||
if (Object.prototype.toString.call(value) === "[object Array]") return value as T[];
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -507,13 +507,13 @@ function importDescription(rawDescription: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof rawDescription === 'string') {
|
||||
if (typeof rawDescription === "string") {
|
||||
return rawDescription;
|
||||
}
|
||||
|
||||
if (typeof rawDescription === 'object' && !Array.isArray(rawDescription)) {
|
||||
if (typeof rawDescription === "object" && !Array.isArray(rawDescription)) {
|
||||
const description = toRecord(rawDescription);
|
||||
if ('content' in description && description.content != null) {
|
||||
if ("content" in description && description.content != null) {
|
||||
return String(description.content);
|
||||
}
|
||||
return undefined;
|
||||
@@ -524,16 +524,16 @@ function importDescription(rawDescription: unknown): string | undefined {
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
if (typeof obj === "string") {
|
||||
return obj.replace(
|
||||
/{{\s*(_\.)?([^}]*)\s*}}/g,
|
||||
(_m, _dot, expr) => `\${[${expr.trim().replace(/^vault:/, '')}]}`,
|
||||
(_m, _dot, expr) => `\${[${expr.trim().replace(/^vault:/, "")}]}`,
|
||||
) as T;
|
||||
}
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
@@ -545,7 +545,7 @@ function deleteUndefinedAttrs<T>(obj: T): T {
|
||||
if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(deleteUndefinedAttrs) as T;
|
||||
}
|
||||
if (typeof obj === 'object' && obj != null) {
|
||||
if (typeof obj === "object" && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { convertPostman } from '../src';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertPostman } from "../src";
|
||||
|
||||
describe('importer-postman', () => {
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
describe("importer-postman", () => {
|
||||
const p = path.join(__dirname, "fixtures");
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
if (fixture.includes('.output')) {
|
||||
if (fixture.includes(".output")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test(`Imports ${fixture}`, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
|
||||
const contents = fs.readFileSync(path.join(p, fixture), "utf-8");
|
||||
const expected = fs.readFileSync(path.join(p, fixture.replace(".input", ".output")), "utf-8");
|
||||
const result = convertPostman(contents);
|
||||
// console.log(JSON.stringify(result, null, 2))
|
||||
expect(JSON.stringify(result, null, 2)).toEqual(
|
||||
@@ -23,21 +23,21 @@ describe('importer-postman', () => {
|
||||
});
|
||||
}
|
||||
|
||||
test('Imports object descriptions without [object Object]', () => {
|
||||
test("Imports object descriptions without [object Object]", () => {
|
||||
const result = convertPostman(
|
||||
JSON.stringify({
|
||||
info: {
|
||||
name: 'Description Test',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
name: "Description Test",
|
||||
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Request 1',
|
||||
name: "Request 1",
|
||||
request: {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
description: {
|
||||
content: 'Lijst van klanten',
|
||||
type: 'text/plain',
|
||||
content: "Lijst van klanten",
|
||||
type: "text/plain",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -47,13 +47,13 @@ describe('importer-postman', () => {
|
||||
|
||||
expect(result?.resources.workspaces).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Description Test',
|
||||
name: "Description Test",
|
||||
}),
|
||||
]);
|
||||
expect(result?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Request 1',
|
||||
description: 'Lijst van klanten',
|
||||
name: "Request 1",
|
||||
description: "Lijst van klanten",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/importer-yaak",
|
||||
"displayName": "Yaak Importer",
|
||||
"description": "Import data from Yaak export files",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import data from Yaak export files",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Environment, PluginDefinition } from '@yaakapp/api';
|
||||
import type { Environment, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'Yaak',
|
||||
description: 'Yaak official format',
|
||||
name: "Yaak",
|
||||
description: "Yaak official format",
|
||||
onImport(_ctx, args) {
|
||||
return migrateImport(args.text);
|
||||
},
|
||||
@@ -23,24 +23,24 @@ export function migrateImport(contents: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isYaakExport = 'yaakSchema' in parsed;
|
||||
const isYaakExport = "yaakSchema" in parsed;
|
||||
if (!isYaakExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate v1 to v2 -- changes requests to httpRequests
|
||||
if ('requests' in parsed.resources) {
|
||||
if ("requests" in parsed.resources) {
|
||||
parsed.resources.httpRequests = parsed.resources.requests;
|
||||
parsed.resources.requests = undefined;
|
||||
}
|
||||
|
||||
// Migrate v2 to v3
|
||||
for (const workspace of parsed.resources.workspaces ?? []) {
|
||||
if ('variables' in workspace) {
|
||||
if ("variables" in workspace) {
|
||||
// Create the base environment
|
||||
const baseEnvironment: Partial<Environment> = {
|
||||
id: `GENERATE_ID::base_env_${workspace.id}`,
|
||||
name: 'Global Variables',
|
||||
name: "Global Variables",
|
||||
variables: workspace.variables,
|
||||
workspaceId: workspace.id,
|
||||
};
|
||||
@@ -61,7 +61,7 @@ export function migrateImport(contents: string) {
|
||||
|
||||
// Migrate v3 to v4
|
||||
for (const environment of parsed.resources.environments ?? []) {
|
||||
if ('environmentId' in environment) {
|
||||
if ("environmentId" in environment) {
|
||||
environment.base = environment.environmentId == null;
|
||||
environment.environmentId = undefined;
|
||||
}
|
||||
@@ -69,12 +69,12 @@ export function migrateImport(contents: string) {
|
||||
|
||||
// Migrate v4 to v5
|
||||
for (const environment of parsed.resources.environments ?? []) {
|
||||
if ('base' in environment && environment.base && environment.parentModel == null) {
|
||||
environment.parentModel = 'workspace';
|
||||
if ("base" in environment && environment.base && environment.parentModel == null) {
|
||||
environment.parentModel = "workspace";
|
||||
environment.parentId = null;
|
||||
environment.base = undefined;
|
||||
} else if ('base' in environment && !environment.base && environment.parentModel == null) {
|
||||
environment.parentModel = 'environment';
|
||||
} else if ("base" in environment && !environment.base && environment.parentModel == null) {
|
||||
environment.parentModel = "environment";
|
||||
environment.parentId = null;
|
||||
environment.base = undefined;
|
||||
}
|
||||
@@ -84,5 +84,5 @@ export function migrateImport(contents: string) {
|
||||
}
|
||||
|
||||
function isJSObject(obj: unknown) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { migrateImport } from '../src';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { migrateImport } from "../src";
|
||||
|
||||
describe('importer-yaak', () => {
|
||||
test('Skips invalid imports', () => {
|
||||
expect(migrateImport('not JSON')).toBeUndefined();
|
||||
expect(migrateImport('[]')).toBeUndefined();
|
||||
describe("importer-yaak", () => {
|
||||
test("Skips invalid imports", () => {
|
||||
expect(migrateImport("not JSON")).toBeUndefined();
|
||||
expect(migrateImport("[]")).toBeUndefined();
|
||||
expect(migrateImport(JSON.stringify({ resources: {} }))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('converts schema 1 to 2', () => {
|
||||
test("converts schema 1 to 2", () => {
|
||||
const imported = migrateImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 1,
|
||||
@@ -26,23 +26,23 @@ describe('importer-yaak', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('converts schema 2 to 3', () => {
|
||||
test("converts schema 2 to 3", () => {
|
||||
const imported = migrateImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 2,
|
||||
resources: {
|
||||
environments: [
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
name: "Production",
|
||||
variables: [{ name: "E1", value: "E1!" }],
|
||||
},
|
||||
],
|
||||
workspaces: [
|
||||
{
|
||||
id: 'w_1',
|
||||
variables: [{ name: 'W1', value: 'W1!' }],
|
||||
id: "w_1",
|
||||
variables: [{ name: "W1", value: "W1!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -54,23 +54,23 @@ describe('importer-yaak', () => {
|
||||
resources: {
|
||||
workspaces: [
|
||||
{
|
||||
id: 'w_1',
|
||||
id: "w_1",
|
||||
},
|
||||
],
|
||||
environments: [
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
parentModel: 'environment',
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
name: "Production",
|
||||
variables: [{ name: "E1", value: "E1!" }],
|
||||
parentModel: "environment",
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
id: 'GENERATE_ID::base_env_w_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Global Variables',
|
||||
variables: [{ name: 'W1', value: 'W1!' }],
|
||||
id: "GENERATE_ID::base_env_w_1",
|
||||
workspaceId: "w_1",
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "W1", value: "W1!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -78,35 +78,35 @@ describe('importer-yaak', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('converts schema 4 to 5', () => {
|
||||
test("converts schema 4 to 5", () => {
|
||||
const imported = migrateImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 2,
|
||||
resources: {
|
||||
environments: [
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
base: false,
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
name: "Production",
|
||||
variables: [{ name: "E1", value: "E1!" }],
|
||||
},
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
base: true,
|
||||
name: 'Global Variables',
|
||||
variables: [{ name: 'G1', value: 'G1!' }],
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "G1", value: "G1!" }],
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
id: 'f_1',
|
||||
id: "f_1",
|
||||
},
|
||||
],
|
||||
workspaces: [
|
||||
{
|
||||
id: 'w_1',
|
||||
id: "w_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -118,30 +118,30 @@ describe('importer-yaak', () => {
|
||||
resources: {
|
||||
workspaces: [
|
||||
{
|
||||
id: 'w_1',
|
||||
id: "w_1",
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
id: 'f_1',
|
||||
id: "f_1",
|
||||
},
|
||||
],
|
||||
environments: [
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Production',
|
||||
variables: [{ name: 'E1', value: 'E1!' }],
|
||||
parentModel: 'environment',
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
name: "Production",
|
||||
variables: [{ name: "E1", value: "E1!" }],
|
||||
parentModel: "environment",
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
id: 'e_1',
|
||||
workspaceId: 'w_1',
|
||||
name: 'Global Variables',
|
||||
parentModel: 'workspace',
|
||||
id: "e_1",
|
||||
workspaceId: "w_1",
|
||||
name: "Global Variables",
|
||||
parentModel: "workspace",
|
||||
parentId: null,
|
||||
variables: [{ name: 'G1', value: 'G1!' }],
|
||||
variables: [{ name: "G1", value: "G1!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-1password",
|
||||
"displayName": "1Password Template Functions",
|
||||
"description": "Template function for accessing 1Password secrets",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template function for accessing 1Password secrets",
|
||||
"scripts": {
|
||||
"build": "run-s build:*",
|
||||
"build:1-build": "yaakcli build",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import crypto from 'node:crypto';
|
||||
import type { Client } from '@1password/sdk';
|
||||
import { createClient, DesktopAuth } from '@1password/sdk';
|
||||
import type { JsonPrimitive, PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
|
||||
import crypto from "node:crypto";
|
||||
import type { Client } from "@1password/sdk";
|
||||
import { createClient, DesktopAuth } from "@1password/sdk";
|
||||
import type { JsonPrimitive, PluginDefinition } from "@yaakapp/api";
|
||||
import type { CallTemplateFunctionArgs } from "@yaakapp-internal/plugins";
|
||||
|
||||
const _clients: Record<string, Client> = {};
|
||||
|
||||
@@ -23,32 +23,32 @@ async function op(
|
||||
let authMethod: string | DesktopAuth;
|
||||
let hash: string;
|
||||
switch (args.values.authMethod) {
|
||||
case 'desktop': {
|
||||
case "desktop": {
|
||||
const account = args.values.token;
|
||||
if (typeof account !== 'string' || !account) return { error: 'Missing account name' };
|
||||
if (typeof account !== "string" || !account) return { error: "Missing account name" };
|
||||
|
||||
hash = crypto.createHash('sha256').update(`desktop:${account}`).digest('hex');
|
||||
hash = crypto.createHash("sha256").update(`desktop:${account}`).digest("hex");
|
||||
authMethod = new DesktopAuth(account);
|
||||
break;
|
||||
}
|
||||
case 'token': {
|
||||
case "token": {
|
||||
const token = args.values.token;
|
||||
if (typeof token !== 'string' || !token) return { error: 'Missing service token' };
|
||||
if (typeof token !== "string" || !token) return { error: "Missing service token" };
|
||||
|
||||
hash = crypto.createHash('sha256').update(`token:${token}`).digest('hex');
|
||||
hash = crypto.createHash("sha256").update(`token:${token}`).digest("hex");
|
||||
authMethod = token;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return { error: 'Invalid authentication method' };
|
||||
return { error: "Invalid authentication method" };
|
||||
}
|
||||
|
||||
if (!_clients[hash]) {
|
||||
try {
|
||||
_clients[hash] = await createClient({
|
||||
auth: authMethod,
|
||||
integrationName: 'Yaak 1Password Plugin',
|
||||
integrationVersion: 'v1.0.0',
|
||||
integrationName: "Yaak 1Password Plugin",
|
||||
integrationVersion: "v1.0.0",
|
||||
});
|
||||
} catch (e) {
|
||||
return { error: e };
|
||||
@@ -66,18 +66,18 @@ async function getValue(
|
||||
fieldId?: JsonPrimitive,
|
||||
): Promise<Result<{ value: string }>> {
|
||||
const res = await op(args);
|
||||
if ('error' in res) return { error: res.error };
|
||||
if ("error" in res) return { error: res.error };
|
||||
const clientHash = res.clientHash;
|
||||
const client = res.client;
|
||||
|
||||
if (!vaultId || typeof vaultId !== 'string') {
|
||||
return { error: 'No vault specified' };
|
||||
if (!vaultId || typeof vaultId !== "string") {
|
||||
return { error: "No vault specified" };
|
||||
}
|
||||
if (!itemId || typeof itemId !== 'string') {
|
||||
return { error: 'No item specified' };
|
||||
if (!itemId || typeof itemId !== "string") {
|
||||
return { error: "No item specified" };
|
||||
}
|
||||
if (!fieldId || typeof fieldId !== 'string') {
|
||||
return { error: 'No field specified' };
|
||||
if (!fieldId || typeof fieldId !== "string") {
|
||||
return { error: "No field specified" };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -98,47 +98,47 @@ async function getValue(
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: '1password.item',
|
||||
description: 'Get a secret',
|
||||
previewArgs: ['field'],
|
||||
name: "1password.item",
|
||||
description: "Get a secret",
|
||||
previewArgs: ["field"],
|
||||
args: [
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
name: 'authMethod',
|
||||
type: 'select',
|
||||
label: 'Authentication Method',
|
||||
defaultValue: 'token',
|
||||
name: "authMethod",
|
||||
type: "select",
|
||||
label: "Authentication Method",
|
||||
defaultValue: "token",
|
||||
options: [
|
||||
{
|
||||
label: 'Service Account',
|
||||
value: 'token',
|
||||
label: "Service Account",
|
||||
value: "token",
|
||||
},
|
||||
{
|
||||
label: 'Desktop App',
|
||||
value: 'desktop',
|
||||
label: "Desktop App",
|
||||
value: "desktop",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'token',
|
||||
type: 'text',
|
||||
name: "token",
|
||||
type: "text",
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- Yaak template syntax
|
||||
defaultValue: '${[1PASSWORD_TOKEN]}',
|
||||
defaultValue: "${[1PASSWORD_TOKEN]}",
|
||||
dynamic(_ctx, args) {
|
||||
switch (args.values.authMethod) {
|
||||
case 'desktop':
|
||||
case "desktop":
|
||||
return {
|
||||
label: 'Account Name',
|
||||
label: "Account Name",
|
||||
description:
|
||||
'Account name can be taken from the sidebar of the 1Password App. Make sure you\'re on the BETA version of the 1Password app and have "Integrate with other apps" enabled in Settings > Developer.',
|
||||
};
|
||||
case 'token':
|
||||
case "token":
|
||||
return {
|
||||
label: 'Token',
|
||||
label: "Token",
|
||||
description:
|
||||
'Token can be generated from the 1Password website by visiting Developer > Service Accounts',
|
||||
"Token can be generated from the 1Password website by visiting Developer > Service Accounts",
|
||||
password: true,
|
||||
};
|
||||
}
|
||||
@@ -149,13 +149,13 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'vault',
|
||||
label: 'Vault',
|
||||
type: 'select',
|
||||
name: "vault",
|
||||
label: "Vault",
|
||||
type: "select",
|
||||
options: [],
|
||||
async dynamic(_ctx, args) {
|
||||
const res = await op(args);
|
||||
if ('error' in res) return { hidden: true };
|
||||
if ("error" in res) return { hidden: true };
|
||||
const clientHash = res.clientHash;
|
||||
const client = res.client;
|
||||
|
||||
@@ -169,9 +169,9 @@ export const plugin: PluginDefinition = {
|
||||
return {
|
||||
options: vaults.map((vault) => {
|
||||
let title = vault.id;
|
||||
if ('title' in vault) {
|
||||
if ("title" in vault) {
|
||||
title = vault.title;
|
||||
} else if ('name' in vault) {
|
||||
} else if ("name" in vault) {
|
||||
// The SDK returns 'name' instead of 'title' but the bindings still use 'title'
|
||||
title = (vault as { name: string }).name;
|
||||
}
|
||||
@@ -185,18 +185,18 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'item',
|
||||
label: 'Item',
|
||||
type: 'select',
|
||||
name: "item",
|
||||
label: "Item",
|
||||
type: "select",
|
||||
options: [],
|
||||
async dynamic(_ctx, args) {
|
||||
const res = await op(args);
|
||||
if ('error' in res) return { hidden: true };
|
||||
if ("error" in res) return { hidden: true };
|
||||
const clientHash = res.clientHash;
|
||||
const client = res.client;
|
||||
|
||||
const vaultId = args.values.vault;
|
||||
if (typeof vaultId !== 'string') return { hidden: true };
|
||||
if (typeof vaultId !== "string") return { hidden: true };
|
||||
|
||||
try {
|
||||
const cacheKey = `${clientHash}:items:${vaultId}`;
|
||||
@@ -216,19 +216,19 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'field',
|
||||
label: 'Field',
|
||||
type: 'select',
|
||||
name: "field",
|
||||
label: "Field",
|
||||
type: "select",
|
||||
options: [],
|
||||
async dynamic(_ctx, args) {
|
||||
const res = await op(args);
|
||||
if ('error' in res) return { hidden: true };
|
||||
if ("error" in res) return { hidden: true };
|
||||
const clientHash = res.clientHash;
|
||||
const client = res.client;
|
||||
|
||||
const vaultId = args.values.vault;
|
||||
const itemId = args.values.item;
|
||||
if (typeof vaultId !== 'string' || typeof itemId !== 'string') {
|
||||
if (typeof vaultId !== "string" || typeof itemId !== "string") {
|
||||
return { hidden: true };
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export const plugin: PluginDefinition = {
|
||||
const itemId = args.values.item;
|
||||
const fieldId = args.values.field;
|
||||
const res = await getValue(args, vaultId, itemId, fieldId);
|
||||
if ('error' in res) {
|
||||
if ("error" in res) {
|
||||
throw res.error;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-cookie",
|
||||
"displayName": "Cookie Template Functions",
|
||||
"description": "Template functions for working with cookies",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with cookies",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'cookie.value',
|
||||
description: 'Read the value of a cookie in the jar, by name',
|
||||
previewArgs: ['name'],
|
||||
name: "cookie.value",
|
||||
description: "Read the value of a cookie in the jar, by name",
|
||||
previewArgs: ["name"],
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Cookie Name',
|
||||
type: "text",
|
||||
name: "name",
|
||||
label: "Cookie Name",
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-ctx",
|
||||
"displayName": "Window Template Functions",
|
||||
"description": "Template functions for accessing attributes of the current window",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for accessing attributes of the current window",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'ctx.request',
|
||||
description: 'Get the ID of the currently active request',
|
||||
name: "ctx.request",
|
||||
description: "Get the ID of the currently active request",
|
||||
args: [],
|
||||
async onRender(ctx) {
|
||||
return ctx.window.requestId();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ctx.environment',
|
||||
description: 'Get the ID of the currently active environment',
|
||||
name: "ctx.environment",
|
||||
description: "Get the ID of the currently active environment",
|
||||
args: [],
|
||||
async onRender(ctx) {
|
||||
return ctx.window.environmentId();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ctx.workspace',
|
||||
description: 'Get the ID of the currently active workspace',
|
||||
name: "ctx.workspace",
|
||||
description: "Get the ID of the currently active workspace",
|
||||
args: [],
|
||||
async onRender(ctx) {
|
||||
return ctx.window.workspaceId();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-encode",
|
||||
"displayName": "Encoding Template Functions",
|
||||
"description": "Template functions for encoding and decoding data",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for encoding and decoding data",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'base64.encode',
|
||||
description: 'Encode a value to base64',
|
||||
name: "base64.encode",
|
||||
description: "Encode a value to base64",
|
||||
args: [
|
||||
{
|
||||
label: 'Encoding',
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
defaultValue: 'base64',
|
||||
label: "Encoding",
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
defaultValue: "base64",
|
||||
options: [
|
||||
{
|
||||
label: 'Base64',
|
||||
value: 'base64',
|
||||
label: "Base64",
|
||||
value: "base64",
|
||||
},
|
||||
{
|
||||
label: 'Base64 URL-safe',
|
||||
value: 'base64url',
|
||||
label: "Base64 URL-safe",
|
||||
value: "base64url",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true },
|
||||
{ label: "Plain Text", type: "text", name: "value", multiLine: true },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return Buffer.from(String(args.values.value ?? '')).toString(
|
||||
args.values.encoding === 'base64url' ? 'base64url' : 'base64',
|
||||
return Buffer.from(String(args.values.value ?? "")).toString(
|
||||
args.values.encoding === "base64url" ? "base64url" : "base64",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'base64.decode',
|
||||
description: 'Decode a value from base64',
|
||||
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
|
||||
name: "base64.decode",
|
||||
description: "Decode a value from base64",
|
||||
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return Buffer.from(String(args.values.value ?? ''), 'base64').toString('utf-8');
|
||||
return Buffer.from(String(args.values.value ?? ""), "base64").toString("utf-8");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'url.encode',
|
||||
description: 'Encode a value for use in a URL (percent-encoding)',
|
||||
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
|
||||
name: "url.encode",
|
||||
description: "Encode a value for use in a URL (percent-encoding)",
|
||||
args: [{ label: "Plain Text", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return encodeURIComponent(String(args.values.value ?? ''));
|
||||
return encodeURIComponent(String(args.values.value ?? ""));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'url.decode',
|
||||
description: 'Decode a percent-encoded URL value',
|
||||
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
|
||||
name: "url.decode",
|
||||
description: "Decode a percent-encoded URL value",
|
||||
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
return decodeURIComponent(String(args.values.value ?? ''));
|
||||
return decodeURIComponent(String(args.values.value ?? ""));
|
||||
} catch {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-fs",
|
||||
"displayName": "File System Template Functions",
|
||||
"description": "Template functions for working with the file system",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with the file system",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import fs from 'node:fs';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import fs from "node:fs";
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
const UTF8 = 'utf8';
|
||||
const UTF8 = "utf8";
|
||||
const options = [
|
||||
{ label: 'ASCII', value: 'ascii' },
|
||||
{ label: 'UTF-8', value: UTF8 },
|
||||
{ label: 'UTF-16 LE', value: 'utf16le' },
|
||||
{ label: 'Base64', value: 'base64' },
|
||||
{ label: 'Base64 URL-safe', value: 'base64url' },
|
||||
{ label: 'Latin-1', value: 'latin1' },
|
||||
{ label: 'Hexadecimal', value: 'hex' },
|
||||
{ label: "ASCII", value: "ascii" },
|
||||
{ label: "UTF-8", value: UTF8 },
|
||||
{ label: "UTF-16 LE", value: "utf16le" },
|
||||
{ label: "Base64", value: "base64" },
|
||||
{ label: "Base64 URL-safe", value: "base64url" },
|
||||
{ label: "Latin-1", value: "latin1" },
|
||||
{ label: "Hexadecimal", value: "hex" },
|
||||
];
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'fs.readFile',
|
||||
description: 'Read the contents of a file as utf-8',
|
||||
previewArgs: ['encoding'],
|
||||
name: "fs.readFile",
|
||||
description: "Read the contents of a file as utf-8",
|
||||
previewArgs: ["encoding"],
|
||||
args: [
|
||||
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
|
||||
{ title: "Select File", type: "file", name: "path", label: "File" },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
label: "Encoding",
|
||||
defaultValue: UTF8,
|
||||
description: "Specifies how the file's bytes are decoded into text when read",
|
||||
options,
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'trim',
|
||||
label: 'Trim Whitespace',
|
||||
description: 'Remove leading and trailing whitespace from the file contents',
|
||||
type: "checkbox",
|
||||
name: "trim",
|
||||
label: "Trim Whitespace",
|
||||
description: "Remove leading and trailing whitespace from the file contents",
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.path || !args.values.encoding) return null;
|
||||
|
||||
try {
|
||||
const v = await fs.promises.readFile(String(args.values.path ?? ''), {
|
||||
encoding: String(args.values.encoding ?? 'utf-8') as BufferEncoding,
|
||||
const v = await fs.promises.readFile(String(args.values.path ?? ""), {
|
||||
encoding: String(args.values.encoding ?? "utf-8") as BufferEncoding,
|
||||
});
|
||||
return args.values.trim ? v.trim() : v;
|
||||
} catch {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-hash",
|
||||
"displayName": "Hash Template Functions",
|
||||
"description": "Template functions for generating hash values",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for generating hash values",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
const algorithms = ['md5', 'sha1', 'sha256', 'sha512'] as const;
|
||||
const encodings = ['base64', 'hex'] as const;
|
||||
const algorithms = ["md5", "sha1", "sha256", "sha512"] as const;
|
||||
const encodings = ["base64", "hex"] as const;
|
||||
|
||||
type TemplateFunctionPlugin = NonNullable<PluginDefinition['templateFunctions']>[number];
|
||||
type TemplateFunctionPlugin = NonNullable<PluginDefinition["templateFunctions"]>[number];
|
||||
|
||||
const hashFunctions: TemplateFunctionPlugin[] = algorithms.map((algorithm) => ({
|
||||
name: `hash.${algorithm}`,
|
||||
description: 'Hash a value to its hexadecimal representation',
|
||||
description: "Hash a value to its hexadecimal representation",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
placeholder: 'input text',
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: "input text",
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
defaultValue: 'base64',
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
label: "Encoding",
|
||||
defaultValue: "base64",
|
||||
options: encodings.map((encoding) => ({
|
||||
label: capitalize(encoding),
|
||||
value: encoding,
|
||||
@@ -32,32 +32,32 @@ const hashFunctions: TemplateFunctionPlugin[] = algorithms.map((algorithm) => ({
|
||||
const input = String(args.values.input);
|
||||
const encoding = String(args.values.encoding) as (typeof encodings)[number];
|
||||
|
||||
return createHash(algorithm).update(input, 'utf-8').digest(encoding);
|
||||
return createHash(algorithm).update(input, "utf-8").digest(encoding);
|
||||
},
|
||||
}));
|
||||
|
||||
const hmacFunctions: TemplateFunctionPlugin[] = algorithms.map((algorithm) => ({
|
||||
name: `hmac.${algorithm}`,
|
||||
description: 'Compute the HMAC of a value',
|
||||
description: "Compute the HMAC of a value",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
placeholder: 'input text',
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: "input text",
|
||||
multiLine: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'key',
|
||||
label: 'Key',
|
||||
type: "text",
|
||||
name: "key",
|
||||
label: "Key",
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
defaultValue: 'base64',
|
||||
type: "select",
|
||||
name: "encoding",
|
||||
label: "Encoding",
|
||||
defaultValue: "base64",
|
||||
options: encodings.map((encoding) => ({
|
||||
value: encoding,
|
||||
label: capitalize(encoding),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-json",
|
||||
"displayName": "JSON Template Functions",
|
||||
"description": "Template functions for working with JSON data",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with JSON data",
|
||||
"main": "build/index.js",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import type { XPathResult } from '@yaak/template-function-xml';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import type { XPathResult } from "@yaak/template-function-xml";
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
const RETURN_FIRST = "first";
|
||||
const RETURN_ALL = "all";
|
||||
const RETURN_JOIN = "join";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'json.jsonpath',
|
||||
description: 'Filter JSON-formatted text using JSONPath syntax',
|
||||
previewArgs: ['query'],
|
||||
name: "json.jsonpath",
|
||||
description: "Filter JSON-formatted text using JSONPath syntax",
|
||||
previewArgs: ["query"],
|
||||
args: [
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
language: 'json',
|
||||
type: "editor",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
language: "json",
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'result',
|
||||
label: 'Return Format',
|
||||
type: "select",
|
||||
name: "result",
|
||||
label: "Return Format",
|
||||
defaultValue: RETURN_FIRST,
|
||||
options: [
|
||||
{ label: 'First result', value: RETURN_FIRST },
|
||||
{ label: 'All results', value: RETURN_ALL },
|
||||
{ label: 'Join with separator', value: RETURN_JOIN },
|
||||
{ label: "First result", value: RETURN_FIRST },
|
||||
{ label: "All results", value: RETURN_ALL },
|
||||
{ label: "Join with separator", value: RETURN_JOIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'join',
|
||||
type: 'text',
|
||||
label: 'Separator',
|
||||
name: "join",
|
||||
type: "text",
|
||||
label: "Separator",
|
||||
optional: true,
|
||||
defaultValue: ', ',
|
||||
defaultValue: ", ",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result !== RETURN_JOIN };
|
||||
},
|
||||
@@ -47,15 +47,15 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'formatted',
|
||||
label: 'Pretty Print',
|
||||
description: 'Format the output as JSON',
|
||||
type: "checkbox",
|
||||
name: "formatted",
|
||||
label: "Pretty Print",
|
||||
description: "Format the output as JSON",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result === RETURN_JOIN };
|
||||
},
|
||||
},
|
||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
|
||||
{ type: "text", name: "query", label: "Query", placeholder: "$..foo" },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
@@ -72,36 +72,36 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'json.escape',
|
||||
description: 'Escape a JSON string, useful when using the output in JSON values',
|
||||
name: "json.escape",
|
||||
description: "Escape a JSON string, useful when using the output in JSON values",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
multiLine: true,
|
||||
placeholder: 'Hello "World"',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const input = String(args.values.input ?? "");
|
||||
return input.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'json.minify',
|
||||
description: 'Remove unnecessary whitespace from a valid JSON string.',
|
||||
name: "json.minify",
|
||||
description: "Remove unnecessary whitespace from a valid JSON string.",
|
||||
args: [
|
||||
{
|
||||
type: 'editor',
|
||||
language: 'json',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
type: "editor",
|
||||
language: "json",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
const input = String(args.values.input ?? "");
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(input));
|
||||
} catch {
|
||||
@@ -112,7 +112,7 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
};
|
||||
|
||||
export type JSONPathResult = 'first' | 'join' | 'all';
|
||||
export type JSONPathResult = "first" | "join" | "all";
|
||||
|
||||
export function filterJSONPath(
|
||||
body: string,
|
||||
@@ -125,15 +125,15 @@ export function filterJSONPath(
|
||||
let items = JSONPath({ path, json: parsed });
|
||||
|
||||
if (items == null) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
// Already good
|
||||
} else if (result === 'first') {
|
||||
items = items[0] ?? '';
|
||||
} else if (result === 'join') {
|
||||
items = items.map((i) => objToStr(i, false)).join(join ?? '');
|
||||
} else if (result === "first") {
|
||||
items = items[0] ?? "";
|
||||
} else if (result === "join") {
|
||||
items = items.map((i) => objToStr(i, false)).join(join ?? "");
|
||||
}
|
||||
|
||||
return objToStr(items, formatted);
|
||||
@@ -141,8 +141,8 @@ export function filterJSONPath(
|
||||
|
||||
function objToStr(o: unknown, formatted = false): string {
|
||||
if (
|
||||
Object.prototype.toString.call(o) === '[object Array]' ||
|
||||
Object.prototype.toString.call(o) === '[object Object]'
|
||||
Object.prototype.toString.call(o) === "[object Array]" ||
|
||||
Object.prototype.toString.call(o) === "[object Object]"
|
||||
) {
|
||||
return formatted ? JSON.stringify(o, null, 2) : JSON.stringify(o);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-prompt",
|
||||
"displayName": "Prompt Template Functions",
|
||||
"description": "Template functions for prompting for user input",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for prompting for user input",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import slugify from 'slugify';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import slugify from "slugify";
|
||||
|
||||
const STORE_NONE = 'none';
|
||||
const STORE_FOREVER = 'forever';
|
||||
const STORE_EXPIRE = 'expire';
|
||||
const STORE_NONE = "none";
|
||||
const STORE_FOREVER = "forever";
|
||||
const STORE_EXPIRE = "expire";
|
||||
|
||||
interface Saved {
|
||||
value: string;
|
||||
@@ -13,15 +13,15 @@ interface Saved {
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'prompt.text',
|
||||
description: 'Prompt the user for input when sending a request',
|
||||
previewType: 'click',
|
||||
previewArgs: ['label'],
|
||||
name: "prompt.text",
|
||||
description: "Prompt the user for input when sending a request",
|
||||
previewType: "click",
|
||||
previewArgs: ["label"],
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: "text",
|
||||
name: "label",
|
||||
label: "Label",
|
||||
optional: true,
|
||||
dynamic(_ctx, args) {
|
||||
if (
|
||||
@@ -33,45 +33,45 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'store',
|
||||
label: 'Store Input',
|
||||
type: "select",
|
||||
name: "store",
|
||||
label: "Store Input",
|
||||
defaultValue: STORE_NONE,
|
||||
options: [
|
||||
{ label: 'Never', value: STORE_NONE },
|
||||
{ label: 'Expire', value: STORE_EXPIRE },
|
||||
{ label: 'Forever', value: STORE_FOREVER },
|
||||
{ label: "Never", value: STORE_NONE },
|
||||
{ label: "Expire", value: STORE_EXPIRE },
|
||||
{ label: "Forever", value: STORE_FOREVER },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.store === STORE_NONE };
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'namespace',
|
||||
label: 'Namespace',
|
||||
type: "text",
|
||||
name: "namespace",
|
||||
label: "Namespace",
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- Yaak template syntax
|
||||
defaultValue: '${[ctx.workspace()]}',
|
||||
defaultValue: "${[ctx.workspace()]}",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'key',
|
||||
label: 'Key (defaults to Label)',
|
||||
type: "text",
|
||||
name: "key",
|
||||
label: "Key (defaults to Label)",
|
||||
optional: true,
|
||||
dynamic(_ctx, args) {
|
||||
return { placeholder: String(args.values.label || '') };
|
||||
return { placeholder: String(args.values.label || "") };
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'ttl',
|
||||
label: 'TTL (seconds)',
|
||||
placeholder: '0',
|
||||
defaultValue: '0',
|
||||
type: "text",
|
||||
name: "ttl",
|
||||
label: "TTL (seconds)",
|
||||
placeholder: "0",
|
||||
defaultValue: "0",
|
||||
optional: true,
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.store !== STORE_EXPIRE };
|
||||
@@ -80,49 +80,49 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'banner',
|
||||
color: 'info',
|
||||
type: "banner",
|
||||
color: "info",
|
||||
inputs: [],
|
||||
dynamic(_ctx, args) {
|
||||
let key: string;
|
||||
try {
|
||||
key = buildKey(args);
|
||||
} catch (err) {
|
||||
return { color: 'danger', inputs: [{ type: 'markdown', content: String(err) }] };
|
||||
return { color: "danger", inputs: [{ type: "markdown", content: String(err) }] };
|
||||
}
|
||||
return {
|
||||
hidden: args.values.store === STORE_NONE,
|
||||
inputs: [
|
||||
{
|
||||
type: 'markdown',
|
||||
content: [`Value will be saved under: \`${key}\``].join('\n\n'),
|
||||
type: "markdown",
|
||||
content: [`Value will be saved under: \`${key}\``].join("\n\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Advanced',
|
||||
type: "accordion",
|
||||
label: "Advanced",
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: 'Prompt Title',
|
||||
type: "text",
|
||||
name: "title",
|
||||
label: "Prompt Title",
|
||||
optional: true,
|
||||
placeholder: 'Enter Value',
|
||||
placeholder: "Enter Value",
|
||||
},
|
||||
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
|
||||
{ type: 'text', name: 'placeholder', label: 'Input Placeholder', optional: true },
|
||||
{ type: 'checkbox', name: 'password', label: 'Mask Value' },
|
||||
{ type: "text", name: "defaultValue", label: "Default Value", optional: true },
|
||||
{ type: "text", name: "placeholder", label: "Input Placeholder", optional: true },
|
||||
{ type: "checkbox", name: "password", label: "Mask Value" },
|
||||
],
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (args.purpose !== 'send') return null;
|
||||
if (args.purpose !== "send") return null;
|
||||
|
||||
if (args.values.store !== STORE_NONE && !args.values.namespace) {
|
||||
throw new Error('Namespace is required when storing values');
|
||||
throw new Error("Namespace is required when storing values");
|
||||
}
|
||||
|
||||
const existing = await maybeGetValue(ctx, args);
|
||||
@@ -131,17 +131,17 @@ export const plugin: PluginDefinition = {
|
||||
}
|
||||
|
||||
const value = await ctx.prompt.text({
|
||||
id: `prompt-${args.values.label ?? 'none'}`,
|
||||
label: String(args.values.label || 'Value'),
|
||||
title: String(args.values.title ?? 'Enter Value'),
|
||||
defaultValue: String(args.values.defaultValue ?? ''),
|
||||
placeholder: String(args.values.placeholder ?? ''),
|
||||
id: `prompt-${args.values.label ?? "none"}`,
|
||||
label: String(args.values.label || "Value"),
|
||||
title: String(args.values.title ?? "Enter Value"),
|
||||
defaultValue: String(args.values.defaultValue ?? ""),
|
||||
placeholder: String(args.values.placeholder ?? ""),
|
||||
password: Boolean(args.values.password),
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (value == null) {
|
||||
throw new Error('Prompt cancelled');
|
||||
throw new Error("Prompt cancelled");
|
||||
}
|
||||
|
||||
if (args.values.store !== STORE_NONE) {
|
||||
@@ -156,12 +156,12 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
function buildKey(args: CallTemplateFunctionArgs) {
|
||||
if (!args.values.key && !args.values.label) {
|
||||
throw new Error('A label or key is required when storing values');
|
||||
throw new Error("A label or key is required when storing values");
|
||||
}
|
||||
return [args.values.namespace, args.values.key || args.values.label]
|
||||
.filter((v) => !!v)
|
||||
.map((v) => slugify(String(v), { lower: true, trim: true }))
|
||||
.join('.');
|
||||
.join(".");
|
||||
}
|
||||
|
||||
async function maybeGetValue(ctx: Context, args: CallTemplateFunctionArgs) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-random",
|
||||
"displayName": "Random Template Functions",
|
||||
"description": "Template functions for generating random values",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for generating random values",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'random.range',
|
||||
description: 'Generate a random number between two values',
|
||||
previewArgs: ['min', 'max'],
|
||||
name: "random.range",
|
||||
description: "Generate a random number between two values",
|
||||
previewArgs: ["min", "max"],
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'min',
|
||||
label: 'Minimum',
|
||||
defaultValue: '0',
|
||||
type: "text",
|
||||
name: "min",
|
||||
label: "Minimum",
|
||||
defaultValue: "0",
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'max',
|
||||
label: 'Maximum',
|
||||
defaultValue: '1',
|
||||
type: "text",
|
||||
name: "max",
|
||||
label: "Maximum",
|
||||
defaultValue: "1",
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'decimals',
|
||||
type: "text",
|
||||
name: "decimals",
|
||||
optional: true,
|
||||
label: 'Decimal Places',
|
||||
label: "Decimal Places",
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const min = args.values.min ? Number.parseInt(String(args.values.min ?? '0'), 10) : 0;
|
||||
const max = args.values.max ? Number.parseInt(String(args.values.max ?? '1'), 10) : 1;
|
||||
const min = args.values.min ? Number.parseInt(String(args.values.min ?? "0"), 10) : 0;
|
||||
const max = args.values.max ? Number.parseInt(String(args.values.max ?? "1"), 10) : 1;
|
||||
const decimals = args.values.decimals
|
||||
? Number.parseInt(String(args.values.decimals ?? '0'), 10)
|
||||
? Number.parseInt(String(args.values.decimals ?? "0"), 10)
|
||||
: null;
|
||||
|
||||
let value = Math.random() * (max - min) + min;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-regex",
|
||||
"displayName": "Regex Template Functions",
|
||||
"description": "Template functions for working with regular expressions",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with regular expressions",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import type { TemplateFunctionArg } from "@yaakapp-internal/plugins";
|
||||
|
||||
const inputArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input Text',
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input Text",
|
||||
multiLine: true,
|
||||
};
|
||||
|
||||
const regexArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'regex',
|
||||
label: 'Regular Expression',
|
||||
placeholder: '\\w+',
|
||||
defaultValue: '.*',
|
||||
type: "text",
|
||||
name: "regex",
|
||||
label: "Regular Expression",
|
||||
placeholder: "\\w+",
|
||||
defaultValue: ".*",
|
||||
description:
|
||||
'A JavaScript regular expression. Use a capture group to reference parts of the match in the replacement.',
|
||||
"A JavaScript regular expression. Use a capture group to reference parts of the match in the replacement.",
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'regex.match',
|
||||
description: 'Extract text using a regular expression',
|
||||
name: "regex.match",
|
||||
description: "Extract text using a regular expression",
|
||||
args: [inputArg, regexArg],
|
||||
previewArgs: [regexArg.name],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
const regex = new RegExp(String(args.values.regex ?? ''));
|
||||
const input = String(args.values.input ?? "");
|
||||
const regex = new RegExp(String(args.values.regex ?? ""));
|
||||
|
||||
const match = input.match(regex);
|
||||
return match?.groups
|
||||
? (Object.values(match.groups)[0] ?? '')
|
||||
: (match?.[1] ?? match?.[0] ?? '');
|
||||
? (Object.values(match.groups)[0] ?? "")
|
||||
: (match?.[1] ?? match?.[0] ?? "");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'regex.replace',
|
||||
description: 'Replace text using a regular expression',
|
||||
name: "regex.replace",
|
||||
description: "Replace text using a regular expression",
|
||||
previewArgs: [regexArg.name],
|
||||
args: [
|
||||
inputArg,
|
||||
regexArg,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'replacement',
|
||||
label: 'Replacement Text',
|
||||
placeholder: 'hello $1',
|
||||
type: "text",
|
||||
name: "replacement",
|
||||
label: "Replacement Text",
|
||||
placeholder: "hello $1",
|
||||
description:
|
||||
'The replacement text. Use $1, $2, ... to reference capture groups or $& to reference the entire match.',
|
||||
"The replacement text. Use $1, $2, ... to reference capture groups or $& to reference the entire match.",
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'flags',
|
||||
label: 'Flags',
|
||||
placeholder: 'g',
|
||||
defaultValue: 'g',
|
||||
type: "text",
|
||||
name: "flags",
|
||||
label: "Flags",
|
||||
placeholder: "g",
|
||||
defaultValue: "g",
|
||||
optional: true,
|
||||
description:
|
||||
'Regular expression flags (g for global, i for case-insensitive, m for multiline, etc.)',
|
||||
"Regular expression flags (g for global, i for case-insensitive, m for multiline, etc.)",
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
const replacement = String(args.values.replacement ?? '');
|
||||
const flags = String(args.values.flags || '');
|
||||
const input = String(args.values.input ?? "");
|
||||
const replacement = String(args.values.replacement ?? "");
|
||||
const flags = String(args.values.flags || "");
|
||||
const regex = String(args.values.regex);
|
||||
|
||||
if (!regex) return '';
|
||||
if (!regex) return "";
|
||||
|
||||
return input.replace(new RegExp(String(args.values.regex), flags), replacement);
|
||||
},
|
||||
|
||||
@@ -1,196 +1,196 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { describe, expect, it } from 'vite-plus/test';
|
||||
import { plugin } from '../src';
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { plugin } from "../src";
|
||||
|
||||
describe('regex.match', () => {
|
||||
const matchFunction = plugin.templateFunctions?.find((f) => f.name === 'regex.match');
|
||||
describe("regex.match", () => {
|
||||
const matchFunction = plugin.templateFunctions?.find((f) => f.name === "regex.match");
|
||||
|
||||
it('should exist', () => {
|
||||
it("should exist", () => {
|
||||
expect(matchFunction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should extract first capture group', async () => {
|
||||
it("should extract first capture group", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello (\\w+)',
|
||||
input: 'Hello World',
|
||||
regex: "Hello (\\w+)",
|
||||
input: "Hello World",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('World');
|
||||
expect(result).toBe("World");
|
||||
});
|
||||
|
||||
it('should extract named capture group', async () => {
|
||||
it("should extract named capture group", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello (?<name>\\w+)',
|
||||
input: 'Hello World',
|
||||
regex: "Hello (?<name>\\w+)",
|
||||
input: "Hello World",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('World');
|
||||
expect(result).toBe("World");
|
||||
});
|
||||
|
||||
it('should return full match when no capture groups', async () => {
|
||||
it("should return full match when no capture groups", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello \\w+',
|
||||
input: 'Hello World',
|
||||
regex: "Hello \\w+",
|
||||
input: "Hello World",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hello World');
|
||||
expect(result).toBe("Hello World");
|
||||
});
|
||||
|
||||
it('should return empty string when no match', async () => {
|
||||
it("should return empty string when no match", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Goodbye',
|
||||
input: 'Hello World',
|
||||
regex: "Goodbye",
|
||||
input: "Hello World",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('');
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it('should return empty string when regex is empty', async () => {
|
||||
it("should return empty string when regex is empty", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '',
|
||||
input: 'Hello World',
|
||||
regex: "",
|
||||
input: "Hello World",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('');
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it('should return empty string when input is empty', async () => {
|
||||
it("should return empty string when input is empty", async () => {
|
||||
const result = await matchFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello',
|
||||
input: '',
|
||||
regex: "Hello",
|
||||
input: "",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('');
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe('regex.replace', () => {
|
||||
const replaceFunction = plugin.templateFunctions?.find((f) => f.name === 'regex.replace');
|
||||
describe("regex.replace", () => {
|
||||
const replaceFunction = plugin.templateFunctions?.find((f) => f.name === "regex.replace");
|
||||
|
||||
it('should exist', () => {
|
||||
it("should exist", () => {
|
||||
expect(replaceFunction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should replace one occurrence by default', async () => {
|
||||
it("should replace one occurrence by default", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'o',
|
||||
input: 'Hello World',
|
||||
replacement: 'a',
|
||||
regex: "o",
|
||||
input: "Hello World",
|
||||
replacement: "a",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hella World');
|
||||
expect(result).toBe("Hella World");
|
||||
});
|
||||
|
||||
it('should replace with capture groups', async () => {
|
||||
it("should replace with capture groups", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '(\\w+) (\\w+)',
|
||||
input: 'Hello World',
|
||||
replacement: '$2 $1',
|
||||
regex: "(\\w+) (\\w+)",
|
||||
input: "Hello World",
|
||||
replacement: "$2 $1",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('World Hello');
|
||||
expect(result).toBe("World Hello");
|
||||
});
|
||||
|
||||
it('should replace with full match reference', async () => {
|
||||
it("should replace with full match reference", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'World',
|
||||
input: 'Hello World',
|
||||
replacement: '[$&]',
|
||||
regex: "World",
|
||||
input: "Hello World",
|
||||
replacement: "[$&]",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hello [World]');
|
||||
expect(result).toBe("Hello [World]");
|
||||
});
|
||||
|
||||
it('should respect flags parameter', async () => {
|
||||
it("should respect flags parameter", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'hello',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi',
|
||||
flags: 'i',
|
||||
regex: "hello",
|
||||
input: "Hello World",
|
||||
replacement: "Hi",
|
||||
flags: "i",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hi World');
|
||||
expect(result).toBe("Hi World");
|
||||
});
|
||||
|
||||
it('should handle empty replacement', async () => {
|
||||
it("should handle empty replacement", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'World',
|
||||
input: 'Hello World',
|
||||
replacement: '',
|
||||
regex: "World",
|
||||
input: "Hello World",
|
||||
replacement: "",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hello ');
|
||||
expect(result).toBe("Hello ");
|
||||
});
|
||||
|
||||
it('should return original input when no match', async () => {
|
||||
it("should return original input when no match", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Goodbye',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi',
|
||||
regex: "Goodbye",
|
||||
input: "Hello World",
|
||||
replacement: "Hi",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('Hello World');
|
||||
expect(result).toBe("Hello World");
|
||||
});
|
||||
|
||||
it('should return empty string when regex is empty', async () => {
|
||||
it("should return empty string when regex is empty", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi',
|
||||
regex: "",
|
||||
input: "Hello World",
|
||||
replacement: "Hi",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('');
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it('should return empty string when input is empty', async () => {
|
||||
it("should return empty string when input is empty", async () => {
|
||||
const result = await replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello',
|
||||
input: '',
|
||||
replacement: 'Hi',
|
||||
regex: "Hello",
|
||||
input: "",
|
||||
replacement: "Hi",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
expect(result).toBe('');
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it('should throw on invalid regex', async () => {
|
||||
it("should throw on invalid regex", async () => {
|
||||
const fn = replaceFunction?.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '[',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi',
|
||||
regex: "[",
|
||||
input: "Hello World",
|
||||
replacement: "Hi",
|
||||
},
|
||||
purpose: 'send',
|
||||
purpose: "send",
|
||||
});
|
||||
await expect(fn).rejects.toThrow(
|
||||
'Invalid regular expression: /[/: Unterminated character class',
|
||||
"Invalid regular expression: /[/: Unterminated character class",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-request",
|
||||
"displayName": "Request Template Functions",
|
||||
"description": "Template functions for extracting value from requests",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for extracting value from requests",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { AnyModel, HttpUrlParameter } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import type { JSONPathResult } from '../../template-function-json';
|
||||
import { filterJSONPath } from '../../template-function-json';
|
||||
import type { XPathResult } from '../../template-function-xml';
|
||||
import { filterXPath } from '../../template-function-xml';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import type { AnyModel, HttpUrlParameter } from "@yaakapp-internal/models";
|
||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||
import type { JSONPathResult } from "../../template-function-json";
|
||||
import { filterJSONPath } from "../../template-function-json";
|
||||
import type { XPathResult } from "../../template-function-xml";
|
||||
import { filterXPath } from "../../template-function-xml";
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
const RETURN_FIRST = "first";
|
||||
const RETURN_ALL = "all";
|
||||
const RETURN_JOIN = "join";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'request.body.raw',
|
||||
aliases: ['request.body'],
|
||||
name: "request.body.raw",
|
||||
aliases: ["request.body"],
|
||||
args: [
|
||||
{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
name: "requestId",
|
||||
label: "Http Request",
|
||||
type: "http_request",
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
return String(
|
||||
await ctx.templates.render({
|
||||
data: httpRequest.body?.text ?? '',
|
||||
data: httpRequest.body?.text ?? "",
|
||||
purpose: args.purpose,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request.body.path',
|
||||
previewArgs: ['path'],
|
||||
name: "request.body.path",
|
||||
previewArgs: ["path"],
|
||||
args: [
|
||||
{ name: 'requestId', label: 'Http Request', type: 'http_request' },
|
||||
{ name: "requestId", label: "Http Request", type: "http_request" },
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'result',
|
||||
label: 'Return Format',
|
||||
type: "select",
|
||||
name: "result",
|
||||
label: "Return Format",
|
||||
defaultValue: RETURN_FIRST,
|
||||
options: [
|
||||
{ label: 'First result', value: RETURN_FIRST },
|
||||
{ label: 'All results', value: RETURN_ALL },
|
||||
{ label: 'Join with separator', value: RETURN_JOIN },
|
||||
{ label: "First result", value: RETURN_FIRST },
|
||||
{ label: "All results", value: RETURN_ALL },
|
||||
{ label: "Join with separator", value: RETURN_JOIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'join',
|
||||
type: 'text',
|
||||
label: 'Separator',
|
||||
name: "join",
|
||||
type: "text",
|
||||
label: "Separator",
|
||||
optional: true,
|
||||
defaultValue: ', ',
|
||||
defaultValue: ", ",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result !== RETURN_JOIN };
|
||||
},
|
||||
@@ -66,52 +66,52 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'path',
|
||||
label: 'JSONPath or XPath',
|
||||
placeholder: '$.books[0].id or /books[0]/id',
|
||||
type: "text",
|
||||
name: "path",
|
||||
label: "JSONPath or XPath",
|
||||
placeholder: "$.books[0].id or /books[0]/id",
|
||||
dynamic: async (ctx, args) => {
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
|
||||
const contentType =
|
||||
httpRequest.headers
|
||||
.find((h) => h.name.toLowerCase() === 'content-type')
|
||||
?.value.toLowerCase() ?? '';
|
||||
if (contentType.includes('xml') || contentType?.includes('html')) {
|
||||
.find((h) => h.name.toLowerCase() === "content-type")
|
||||
?.value.toLowerCase() ?? "";
|
||||
if (contentType.includes("xml") || contentType?.includes("html")) {
|
||||
return {
|
||||
label: 'XPath',
|
||||
placeholder: '/books[0]/id',
|
||||
description: 'Enter an XPath expression used to filter the results',
|
||||
label: "XPath",
|
||||
placeholder: "/books[0]/id",
|
||||
description: "Enter an XPath expression used to filter the results",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'JSONPath',
|
||||
placeholder: '$.books[0].id',
|
||||
description: 'Enter a JSONPath expression used to filter the results',
|
||||
label: "JSONPath",
|
||||
placeholder: "$.books[0].id",
|
||||
description: "Enter a JSONPath expression used to filter the results",
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
|
||||
const body = httpRequest.body?.text ?? '';
|
||||
const body = httpRequest.body?.text ?? "";
|
||||
|
||||
try {
|
||||
const result: JSONPathResult =
|
||||
args.values.result === RETURN_ALL
|
||||
? 'all'
|
||||
? "all"
|
||||
: args.values.result === RETURN_JOIN
|
||||
? 'join'
|
||||
: 'first';
|
||||
? "join"
|
||||
: "first";
|
||||
return filterJSONPath(
|
||||
body,
|
||||
String(args.values.path || ''),
|
||||
String(args.values.path || ""),
|
||||
result,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
);
|
||||
@@ -122,13 +122,13 @@ export const plugin: PluginDefinition = {
|
||||
try {
|
||||
const result: XPathResult =
|
||||
args.values.result === RETURN_ALL
|
||||
? 'all'
|
||||
? "all"
|
||||
: args.values.result === RETURN_JOIN
|
||||
? 'join'
|
||||
: 'first';
|
||||
? "join"
|
||||
: "first";
|
||||
return filterXPath(
|
||||
body,
|
||||
String(args.values.path || ''),
|
||||
String(args.values.path || ""),
|
||||
result,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
);
|
||||
@@ -140,21 +140,21 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request.header',
|
||||
description: 'Read the value of a request header, by name',
|
||||
previewArgs: ['header'],
|
||||
name: "request.header",
|
||||
description: "Read the value of a request header, by name",
|
||||
previewArgs: ["header"],
|
||||
args: [
|
||||
{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
name: "requestId",
|
||||
label: "Http Request",
|
||||
type: "http_request",
|
||||
},
|
||||
{
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
type: 'text',
|
||||
name: "header",
|
||||
label: "Header Name",
|
||||
type: "text",
|
||||
async dynamic(ctx, args) {
|
||||
if (typeof args.values.requestId !== 'string') return null;
|
||||
if (typeof args.values.requestId !== "string") return null;
|
||||
|
||||
const request = await ctx.httpRequest.getById({ id: args.values.requestId });
|
||||
if (request == null) return null;
|
||||
@@ -164,15 +164,15 @@ export const plugin: PluginDefinition = {
|
||||
placeholder: validHeaders[0]?.name,
|
||||
completionOptions: validHeaders.map<GenericCompletionOption>((h) => ({
|
||||
label: h.name,
|
||||
type: 'constant',
|
||||
type: "constant",
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const headerName = String(args.values.header ?? '');
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const headerName = String(args.values.header ?? "");
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
const header = httpRequest.headers.find(
|
||||
@@ -180,29 +180,29 @@ export const plugin: PluginDefinition = {
|
||||
);
|
||||
return String(
|
||||
await ctx.templates.render({
|
||||
data: header?.value ?? '',
|
||||
data: header?.value ?? "",
|
||||
purpose: args.purpose,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request.param',
|
||||
name: "request.param",
|
||||
args: [
|
||||
{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
name: "requestId",
|
||||
label: "Http Request",
|
||||
type: "http_request",
|
||||
},
|
||||
{
|
||||
name: 'param',
|
||||
label: 'Param Name',
|
||||
type: 'text',
|
||||
name: "param",
|
||||
label: "Param Name",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const paramName = String(args.values.param ?? '');
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const paramName = String(args.values.param ?? "");
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
|
||||
@@ -211,7 +211,7 @@ export const plugin: PluginDefinition = {
|
||||
purpose: args.purpose,
|
||||
});
|
||||
|
||||
const querystring = renderedUrl.split('?')[1] ?? '';
|
||||
const querystring = renderedUrl.split("?")[1] ?? "";
|
||||
const paramsFromUrl: HttpUrlParameter[] = new URLSearchParams(querystring)
|
||||
.entries()
|
||||
.map(([name, value]): HttpUrlParameter => ({ name, value }))
|
||||
@@ -222,23 +222,23 @@ export const plugin: PluginDefinition = {
|
||||
const foundParam = allEnabledParams.find((p) => p.name === paramName);
|
||||
|
||||
const renderedValue = await ctx.templates.render({
|
||||
data: foundParam?.value ?? '',
|
||||
data: foundParam?.value ?? "",
|
||||
purpose: args.purpose,
|
||||
});
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request.name',
|
||||
name: "request.name",
|
||||
args: [
|
||||
{
|
||||
name: 'requestId',
|
||||
label: 'Http Request',
|
||||
type: 'http_request',
|
||||
name: "requestId",
|
||||
label: "Http Request",
|
||||
type: "http_request",
|
||||
},
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const requestId = String(args.values.requestId ?? 'n/a');
|
||||
const requestId = String(args.values.requestId ?? "n/a");
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
|
||||
if (httpRequest == null) return null;
|
||||
|
||||
@@ -250,37 +250,37 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
// TODO: Use a common function for this, but it fails to build on windows during CI if I try importing it here
|
||||
export function resolvedModelName(r: AnyModel | null): string {
|
||||
if (r == null) return '';
|
||||
if (r == null) return "";
|
||||
|
||||
if (!('url' in r) || r.model === 'plugin') {
|
||||
return 'name' in r ? r.name : '';
|
||||
if (!("url" in r) || r.model === "plugin") {
|
||||
return "name" in r ? r.name : "";
|
||||
}
|
||||
|
||||
// Return name if it has one
|
||||
if ('name' in r && r.name) {
|
||||
if ("name" in r && r.name) {
|
||||
return r.name;
|
||||
}
|
||||
|
||||
// Replace variable syntax with variable name
|
||||
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, '$1');
|
||||
if (withoutVariables.trim() === '') {
|
||||
return r.model === 'http_request'
|
||||
? r.bodyType && r.bodyType === 'graphql'
|
||||
? 'GraphQL Request'
|
||||
: 'HTTP Request'
|
||||
: r.model === 'websocket_request'
|
||||
? 'WebSocket Request'
|
||||
: 'gRPC Request';
|
||||
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, "$1");
|
||||
if (withoutVariables.trim() === "") {
|
||||
return r.model === "http_request"
|
||||
? r.bodyType && r.bodyType === "graphql"
|
||||
? "GraphQL Request"
|
||||
: "HTTP Request"
|
||||
: r.model === "websocket_request"
|
||||
? "WebSocket Request"
|
||||
: "gRPC Request";
|
||||
}
|
||||
|
||||
// GRPC gets nice short names
|
||||
if (r.model === 'grpc_request' && r.service != null && r.method != null) {
|
||||
const shortService = r.service.split('.').pop();
|
||||
if (r.model === "grpc_request" && r.service != null && r.method != null) {
|
||||
const shortService = r.service.split(".").pop();
|
||||
return `${shortService}/${r.method}`;
|
||||
}
|
||||
|
||||
// Strip unnecessary protocol
|
||||
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, '');
|
||||
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, "");
|
||||
|
||||
return withoutProto;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-response",
|
||||
"displayName": "Response Template Functions",
|
||||
"description": "Template functions for request chaining",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for request chaining",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync } from "node:fs";
|
||||
import type {
|
||||
CallTemplateFunctionArgs,
|
||||
Context,
|
||||
@@ -7,41 +7,41 @@ import type {
|
||||
HttpResponse,
|
||||
PluginDefinition,
|
||||
RenderPurpose,
|
||||
} from '@yaakapp/api';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import type { JSONPathResult } from '../../template-function-json';
|
||||
import { filterJSONPath } from '../../template-function-json';
|
||||
import type { XPathResult } from '../../template-function-xml';
|
||||
import { filterXPath } from '../../template-function-xml';
|
||||
} from "@yaakapp/api";
|
||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||
import type { JSONPathResult } from "../../template-function-json";
|
||||
import { filterJSONPath } from "../../template-function-json";
|
||||
import type { XPathResult } from "../../template-function-xml";
|
||||
import { filterXPath } from "../../template-function-xml";
|
||||
|
||||
const BEHAVIOR_TTL = 'ttl';
|
||||
const BEHAVIOR_ALWAYS = 'always';
|
||||
const BEHAVIOR_SMART = 'smart';
|
||||
const BEHAVIOR_TTL = "ttl";
|
||||
const BEHAVIOR_ALWAYS = "always";
|
||||
const BEHAVIOR_SMART = "smart";
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
const RETURN_FIRST = "first";
|
||||
const RETURN_ALL = "all";
|
||||
const RETURN_JOIN = "join";
|
||||
|
||||
const behaviorArgs: DynamicTemplateFunctionArg = {
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'behavior',
|
||||
label: 'Sending Behavior',
|
||||
type: "select",
|
||||
name: "behavior",
|
||||
label: "Sending Behavior",
|
||||
defaultValue: BEHAVIOR_SMART,
|
||||
options: [
|
||||
{ label: 'When no responses', value: BEHAVIOR_SMART },
|
||||
{ label: 'Always', value: BEHAVIOR_ALWAYS },
|
||||
{ label: 'When expired', value: BEHAVIOR_TTL },
|
||||
{ label: "When no responses", value: BEHAVIOR_SMART },
|
||||
{ label: "Always", value: BEHAVIOR_ALWAYS },
|
||||
{ label: "When expired", value: BEHAVIOR_TTL },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'ttl',
|
||||
label: 'TTL (seconds)',
|
||||
placeholder: '0',
|
||||
defaultValue: '0',
|
||||
type: "text",
|
||||
name: "ttl",
|
||||
label: "TTL (seconds)",
|
||||
placeholder: "0",
|
||||
defaultValue: "0",
|
||||
description:
|
||||
'Resend the request when the latest response is older than this many seconds, or if there are no responses yet. "0" means never expires',
|
||||
dynamic(_ctx, args) {
|
||||
@@ -52,42 +52,42 @@ const behaviorArgs: DynamicTemplateFunctionArg = {
|
||||
};
|
||||
|
||||
const requestArg: FormInput = {
|
||||
type: 'http_request',
|
||||
name: 'request',
|
||||
label: 'Request',
|
||||
defaultValue: '', // Make it not select the active one by default
|
||||
type: "http_request",
|
||||
name: "request",
|
||||
label: "Request",
|
||||
defaultValue: "", // Make it not select the active one by default
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'response.header',
|
||||
description: 'Read the value of a response header, by name',
|
||||
previewArgs: ['header'],
|
||||
name: "response.header",
|
||||
description: "Read the value of a response header, by name",
|
||||
previewArgs: ["header"],
|
||||
args: [
|
||||
requestArg,
|
||||
behaviorArgs,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
type: "text",
|
||||
name: "header",
|
||||
label: "Header Name",
|
||||
async dynamic(ctx, args) {
|
||||
// Dynamic form config also runs during send-time rendering.
|
||||
// Keep this preview-only to avoid side-effect request sends.
|
||||
if (args.purpose !== 'preview') return null;
|
||||
if (args.purpose !== "preview") return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
requestId: String(args.values.request || ""),
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||
ttl: String(args.values.ttl || ''),
|
||||
ttl: String(args.values.ttl || ""),
|
||||
});
|
||||
|
||||
return {
|
||||
placeholder: response?.headers[0]?.name,
|
||||
completionOptions: response?.headers.map<GenericCompletionOption>((h) => ({
|
||||
label: h.name,
|
||||
type: 'constant',
|
||||
type: "constant",
|
||||
})),
|
||||
};
|
||||
},
|
||||
@@ -97,47 +97,47 @@ export const plugin: PluginDefinition = {
|
||||
if (!args.values.request || !args.values.header) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
requestId: String(args.values.request || ""),
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||
ttl: String(args.values.ttl || ''),
|
||||
ttl: String(args.values.ttl || ""),
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
const header = response.headers.find(
|
||||
(h) => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(),
|
||||
(h) => h.name.toLowerCase() === String(args.values.header ?? "").toLowerCase(),
|
||||
);
|
||||
return header?.value ?? null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'response.body.path',
|
||||
description: 'Access a field of the response body using JsonPath or XPath',
|
||||
aliases: ['response'],
|
||||
previewArgs: ['path'],
|
||||
name: "response.body.path",
|
||||
description: "Access a field of the response body using JsonPath or XPath",
|
||||
aliases: ["response"],
|
||||
previewArgs: ["path"],
|
||||
args: [
|
||||
requestArg,
|
||||
behaviorArgs,
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'result',
|
||||
label: 'Return Format',
|
||||
type: "select",
|
||||
name: "result",
|
||||
label: "Return Format",
|
||||
defaultValue: RETURN_FIRST,
|
||||
options: [
|
||||
{ label: 'First result', value: RETURN_FIRST },
|
||||
{ label: 'All results', value: RETURN_ALL },
|
||||
{ label: 'Join with separator', value: RETURN_JOIN },
|
||||
{ label: "First result", value: RETURN_FIRST },
|
||||
{ label: "All results", value: RETURN_ALL },
|
||||
{ label: "Join with separator", value: RETURN_JOIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'join',
|
||||
type: 'text',
|
||||
label: 'Separator',
|
||||
name: "join",
|
||||
type: "text",
|
||||
label: "Separator",
|
||||
optional: true,
|
||||
defaultValue: ', ',
|
||||
defaultValue: ", ",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result !== RETURN_JOIN };
|
||||
},
|
||||
@@ -145,20 +145,20 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'path',
|
||||
label: 'JSONPath or XPath',
|
||||
placeholder: '$.books[0].id or /books[0]/id',
|
||||
type: "text",
|
||||
name: "path",
|
||||
label: "JSONPath or XPath",
|
||||
placeholder: "$.books[0].id or /books[0]/id",
|
||||
dynamic: async (ctx, args) => {
|
||||
// Dynamic form config also runs during send-time rendering.
|
||||
// Keep this preview-only to avoid side-effect request sends.
|
||||
if (args.purpose !== 'preview') return null;
|
||||
if (args.purpose !== "preview") return null;
|
||||
|
||||
const resp = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
purpose: 'preview',
|
||||
requestId: String(args.values.request || ""),
|
||||
purpose: "preview",
|
||||
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||
ttl: String(args.values.ttl || ''),
|
||||
ttl: String(args.values.ttl || ""),
|
||||
});
|
||||
|
||||
if (resp == null) {
|
||||
@@ -167,20 +167,20 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
const contentType =
|
||||
resp?.headers
|
||||
.find((h) => h.name.toLowerCase() === 'content-type')
|
||||
?.value.toLowerCase() ?? '';
|
||||
if (contentType.includes('xml') || contentType?.includes('html')) {
|
||||
.find((h) => h.name.toLowerCase() === "content-type")
|
||||
?.value.toLowerCase() ?? "";
|
||||
if (contentType.includes("xml") || contentType?.includes("html")) {
|
||||
return {
|
||||
label: 'XPath',
|
||||
placeholder: '/books[0]/id',
|
||||
description: 'Enter an XPath expression used to filter the results',
|
||||
label: "XPath",
|
||||
placeholder: "/books[0]/id",
|
||||
description: "Enter an XPath expression used to filter the results",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'JSONPath',
|
||||
placeholder: '$.books[0].id',
|
||||
description: 'Enter a JSONPath expression used to filter the results',
|
||||
label: "JSONPath",
|
||||
placeholder: "$.books[0].id",
|
||||
description: "Enter a JSONPath expression used to filter the results",
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -189,10 +189,10 @@ export const plugin: PluginDefinition = {
|
||||
if (!args.values.request || !args.values.path) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
requestId: String(args.values.request || ""),
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||
ttl: String(args.values.ttl || ''),
|
||||
ttl: String(args.values.ttl || ""),
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
@@ -200,10 +200,10 @@ export const plugin: PluginDefinition = {
|
||||
return null;
|
||||
}
|
||||
|
||||
const BOM = '\ufeff';
|
||||
const BOM = "\ufeff";
|
||||
let body: string;
|
||||
try {
|
||||
body = readFileSync(response.bodyPath, 'utf-8').replace(BOM, '');
|
||||
body = readFileSync(response.bodyPath, "utf-8").replace(BOM, "");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -211,13 +211,13 @@ export const plugin: PluginDefinition = {
|
||||
try {
|
||||
const result: JSONPathResult =
|
||||
args.values.result === RETURN_ALL
|
||||
? 'all'
|
||||
? "all"
|
||||
: args.values.result === RETURN_JOIN
|
||||
? 'join'
|
||||
: 'first';
|
||||
? "join"
|
||||
: "first";
|
||||
return filterJSONPath(
|
||||
body,
|
||||
String(args.values.path || ''),
|
||||
String(args.values.path || ""),
|
||||
result,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
);
|
||||
@@ -228,13 +228,13 @@ export const plugin: PluginDefinition = {
|
||||
try {
|
||||
const result: XPathResult =
|
||||
args.values.result === RETURN_ALL
|
||||
? 'all'
|
||||
? "all"
|
||||
: args.values.result === RETURN_JOIN
|
||||
? 'join'
|
||||
: 'first';
|
||||
? "join"
|
||||
: "first";
|
||||
return filterXPath(
|
||||
body,
|
||||
String(args.values.path || ''),
|
||||
String(args.values.path || ""),
|
||||
result,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
);
|
||||
@@ -246,18 +246,18 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'response.body.raw',
|
||||
description: 'Access the entire response body, as text',
|
||||
aliases: ['response'],
|
||||
name: "response.body.raw",
|
||||
description: "Access the entire response body, as text",
|
||||
aliases: ["response"],
|
||||
args: [requestArg, behaviorArgs],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.request) return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
requestId: String(args.values.request || ""),
|
||||
purpose: args.purpose,
|
||||
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||
ttl: String(args.values.ttl || ''),
|
||||
ttl: String(args.values.ttl || ""),
|
||||
});
|
||||
if (response == null) return null;
|
||||
|
||||
@@ -267,7 +267,7 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
let body: string;
|
||||
try {
|
||||
body = readFileSync(response.bodyPath, 'utf-8');
|
||||
body = readFileSync(response.bodyPath, "utf-8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -294,14 +294,14 @@ async function getResponse(
|
||||
): Promise<HttpResponse | null> {
|
||||
if (!requestId) return null;
|
||||
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' });
|
||||
const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? "n/a" });
|
||||
if (httpRequest == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const responses = await ctx.httpResponse.find({ requestId: httpRequest.id, limit: 1 });
|
||||
|
||||
if (behavior === 'never' && responses.length === 0) {
|
||||
if (behavior === "never" && responses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -309,12 +309,12 @@ async function getResponse(
|
||||
|
||||
// Previews happen a ton, and we don't want to send too many times on "always," so treat
|
||||
// it as "smart" during preview.
|
||||
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
|
||||
const finalBehavior = behavior === "always" && purpose === "preview" ? "smart" : behavior;
|
||||
|
||||
// Send if no responses and "smart," or "always"
|
||||
if (
|
||||
(finalBehavior === 'smart' && response == null) ||
|
||||
finalBehavior === 'always' ||
|
||||
(finalBehavior === "smart" && response == null) ||
|
||||
finalBehavior === "always" ||
|
||||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
|
||||
) {
|
||||
// Explicitly render the request before send (instead of relying on send() to render) so that we can
|
||||
@@ -328,7 +328,7 @@ async function getResponse(
|
||||
|
||||
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
|
||||
if (response == null) return true;
|
||||
const ttlSeconds = Number.parseInt(ttl || '0', 10) || 0;
|
||||
const ttlSeconds = Number.parseInt(ttl || "0", 10) || 0;
|
||||
if (ttlSeconds === 0) return false;
|
||||
const nowMillis = Date.now();
|
||||
const respMillis = new Date(`${response.createdAt}Z`).getTime();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-timestamp",
|
||||
"displayName": "Timestamp Template Functions",
|
||||
"description": "Template functions for dealing with timestamps",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for dealing with timestamps",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
import type { TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||
import type { PluginDefinition } from "@yaakapp/api";
|
||||
import type { TemplateFunctionArg } from "@yaakapp-internal/plugins";
|
||||
|
||||
import type { ContextFn } from 'date-fns';
|
||||
import type { ContextFn } from "date-fns";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
@@ -18,75 +18,75 @@ import {
|
||||
subMonths,
|
||||
subSeconds,
|
||||
subYears,
|
||||
} from 'date-fns';
|
||||
} from "date-fns";
|
||||
|
||||
const dateArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'date',
|
||||
label: 'Timestamp',
|
||||
type: "text",
|
||||
name: "date",
|
||||
label: "Timestamp",
|
||||
optional: true,
|
||||
description:
|
||||
'Can be a timestamp in milliseconds, ISO string, or anything parseable by JS `new Date()`',
|
||||
"Can be a timestamp in milliseconds, ISO string, or anything parseable by JS `new Date()`",
|
||||
placeholder: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const expressionArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'expression',
|
||||
label: 'Expression',
|
||||
type: "text",
|
||||
name: "expression",
|
||||
label: "Expression",
|
||||
description: "Modification expression (eg. '-5d +2h 3m'). Available units: y, M, d, h, m, s",
|
||||
optional: true,
|
||||
placeholder: '-5d +2h 3m',
|
||||
placeholder: "-5d +2h 3m",
|
||||
};
|
||||
|
||||
const formatArg: TemplateFunctionArg = {
|
||||
name: 'format',
|
||||
label: 'Format String',
|
||||
name: "format",
|
||||
label: "Format String",
|
||||
description: "Format string to describe the output (eg. 'yyyy-MM-dd at HH:mm:ss')",
|
||||
optional: true,
|
||||
placeholder: 'yyyy-MM-dd HH:mm:ss',
|
||||
type: 'text',
|
||||
placeholder: "yyyy-MM-dd HH:mm:ss",
|
||||
type: "text",
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'timestamp.unix',
|
||||
description: 'Get the timestamp in seconds',
|
||||
name: "timestamp.unix",
|
||||
description: "Get the timestamp in seconds",
|
||||
args: [dateArg],
|
||||
onRender: async (_ctx, args) => {
|
||||
const d = parseDateString(String(args.values.date ?? ''));
|
||||
const d = parseDateString(String(args.values.date ?? ""));
|
||||
return String(Math.floor(d.getTime() / 1000));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timestamp.unixMillis',
|
||||
description: 'Get the timestamp in milliseconds',
|
||||
name: "timestamp.unixMillis",
|
||||
description: "Get the timestamp in milliseconds",
|
||||
args: [dateArg],
|
||||
onRender: async (_ctx, args) => {
|
||||
const d = parseDateString(String(args.values.date ?? ''));
|
||||
const d = parseDateString(String(args.values.date ?? ""));
|
||||
return String(d.getTime());
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timestamp.iso8601',
|
||||
description: 'Get the date in ISO8601 format',
|
||||
name: "timestamp.iso8601",
|
||||
description: "Get the date in ISO8601 format",
|
||||
args: [dateArg],
|
||||
onRender: async (_ctx, args) => {
|
||||
const d = parseDateString(String(args.values.date ?? ''));
|
||||
const d = parseDateString(String(args.values.date ?? ""));
|
||||
return d.toISOString();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timestamp.format',
|
||||
description: 'Format a date using a dayjs-compatible format string',
|
||||
name: "timestamp.format",
|
||||
description: "Format a date using a dayjs-compatible format string",
|
||||
args: [dateArg, formatArg],
|
||||
previewArgs: [formatArg.name],
|
||||
onRender: async (_ctx, args) => formatDatetime(args.values),
|
||||
},
|
||||
{
|
||||
name: 'timestamp.offset',
|
||||
description: 'Get the offset of a date based on an expression',
|
||||
name: "timestamp.offset",
|
||||
description: "Get the offset of a date based on an expression",
|
||||
args: [dateArg, expressionArg],
|
||||
previewArgs: [expressionArg.name],
|
||||
onRender: async (_ctx, args) => calculateDatetime(args.values),
|
||||
@@ -96,18 +96,18 @@ export const plugin: PluginDefinition = {
|
||||
|
||||
function applyDateOp(d: Date, sign: string, amount: number, unit: string): Date {
|
||||
switch (unit) {
|
||||
case 'y':
|
||||
return sign === '-' ? subYears(d, amount) : addYears(d, amount);
|
||||
case 'M':
|
||||
return sign === '-' ? subMonths(d, amount) : addMonths(d, amount);
|
||||
case 'd':
|
||||
return sign === '-' ? subDays(d, amount) : addDays(d, amount);
|
||||
case 'h':
|
||||
return sign === '-' ? subHours(d, amount) : addHours(d, amount);
|
||||
case 'm':
|
||||
return sign === '-' ? subMinutes(d, amount) : addMinutes(d, amount);
|
||||
case 's':
|
||||
return sign === '-' ? subSeconds(d, amount) : addSeconds(d, amount);
|
||||
case "y":
|
||||
return sign === "-" ? subYears(d, amount) : addYears(d, amount);
|
||||
case "M":
|
||||
return sign === "-" ? subMonths(d, amount) : addMonths(d, amount);
|
||||
case "d":
|
||||
return sign === "-" ? subDays(d, amount) : addDays(d, amount);
|
||||
case "h":
|
||||
return sign === "-" ? subHours(d, amount) : addHours(d, amount);
|
||||
case "m":
|
||||
return sign === "-" ? subMinutes(d, amount) : addMinutes(d, amount);
|
||||
case "s":
|
||||
return sign === "-" ? subSeconds(d, amount) : addSeconds(d, amount);
|
||||
default:
|
||||
throw new Error(`Invalid data calculation unit: ${unit}`);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ function parseOp(op: string): { sign: string; amount: number; unit: string } | n
|
||||
}
|
||||
const [, sign, amount, unit] = match;
|
||||
if (!unit) return null;
|
||||
return { sign: sign ?? '+', amount: Number(amount ?? 0), unit };
|
||||
return { sign: sign ?? "+", amount: Number(amount ?? 0), unit };
|
||||
}
|
||||
|
||||
function parseDateString(date: string): Date {
|
||||
@@ -143,11 +143,11 @@ function parseDateString(date: string): Date {
|
||||
|
||||
export function calculateDatetime(args: { date?: string; expression?: string }): string {
|
||||
const { date, expression } = args;
|
||||
let jsDate = parseDateString(date ?? '');
|
||||
let jsDate = parseDateString(date ?? "");
|
||||
|
||||
if (expression) {
|
||||
const ops = String(expression)
|
||||
.split(' ')
|
||||
.split(" ")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const op of ops) {
|
||||
@@ -167,6 +167,6 @@ export function formatDatetime(args: {
|
||||
in?: ContextFn<Date>;
|
||||
}): string {
|
||||
const { date, format } = args;
|
||||
const d = parseDateString(date ?? '');
|
||||
return formatDate(d, String(format || 'yyyy-MM-dd HH:mm:ss'), { in: args.in });
|
||||
const d = parseDateString(date ?? "");
|
||||
return formatDate(d, String(format || "yyyy-MM-dd HH:mm:ss"), { in: args.in });
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { tz } from '@date-fns/tz';
|
||||
import { describe, expect, it } from 'vite-plus/test';
|
||||
import { calculateDatetime, formatDatetime } from '../src';
|
||||
import { tz } from "@date-fns/tz";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { calculateDatetime, formatDatetime } from "../src";
|
||||
|
||||
describe('formatDatetime', () => {
|
||||
it('returns formatted current date', () => {
|
||||
describe("formatDatetime", () => {
|
||||
it("returns formatted current date", () => {
|
||||
const result = formatDatetime({});
|
||||
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it('returns formatted specific date', () => {
|
||||
const result = formatDatetime({ date: '2025-07-13T12:34:56' });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
it("returns formatted specific date", () => {
|
||||
const result = formatDatetime({ date: "2025-07-13T12:34:56" });
|
||||
expect(result).toBe("2025-07-13 12:34:56");
|
||||
});
|
||||
|
||||
it('returns formatted specific timestamp', () => {
|
||||
const result = formatDatetime({ date: '1752435296000', in: tz('America/Vancouver') });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
it("returns formatted specific timestamp", () => {
|
||||
const result = formatDatetime({ date: "1752435296000", in: tz("America/Vancouver") });
|
||||
expect(result).toBe("2025-07-13 12:34:56");
|
||||
});
|
||||
|
||||
it('returns formatted specific timestamp with decimals', () => {
|
||||
const result = formatDatetime({ date: '1752435296000.19', in: tz('America/Vancouver') });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
it("returns formatted specific timestamp with decimals", () => {
|
||||
const result = formatDatetime({ date: "1752435296000.19", in: tz("America/Vancouver") });
|
||||
expect(result).toBe("2025-07-13 12:34:56");
|
||||
});
|
||||
|
||||
it('returns formatted date with custom output', () => {
|
||||
const result = formatDatetime({ date: '2025-07-13T12:34:56', format: 'dd/MM/yyyy' });
|
||||
expect(result).toBe('13/07/2025');
|
||||
it("returns formatted date with custom output", () => {
|
||||
const result = formatDatetime({ date: "2025-07-13T12:34:56", format: "dd/MM/yyyy" });
|
||||
expect(result).toBe("13/07/2025");
|
||||
});
|
||||
|
||||
it('handles invalid date gracefully', () => {
|
||||
expect(() => formatDatetime({ date: 'invalid-date' })).toThrow('Invalid date: invalid-date');
|
||||
it("handles invalid date gracefully", () => {
|
||||
expect(() => formatDatetime({ date: "invalid-date" })).toThrow("Invalid date: invalid-date");
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDatetime', () => {
|
||||
it('returns ISO string for current date', () => {
|
||||
describe("calculateDatetime", () => {
|
||||
it("returns ISO string for current date", () => {
|
||||
const result = calculateDatetime({});
|
||||
expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it('returns ISO string for specific date', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:34:56Z' });
|
||||
expect(result).toBe('2025-07-13T12:34:56.000Z');
|
||||
it("returns ISO string for specific date", () => {
|
||||
const result = calculateDatetime({ date: "2025-07-13T12:34:56Z" });
|
||||
expect(result).toBe("2025-07-13T12:34:56.000Z");
|
||||
});
|
||||
|
||||
it('applies calc operations', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1d 2h' });
|
||||
expect(result).toBe('2025-07-14T14:00:00.000Z');
|
||||
it("applies calc operations", () => {
|
||||
const result = calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1d 2h" });
|
||||
expect(result).toBe("2025-07-14T14:00:00.000Z");
|
||||
});
|
||||
|
||||
it('applies negative calc operations', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '-1d -2h 1m' });
|
||||
expect(result).toBe('2025-07-12T10:01:00.000Z');
|
||||
it("applies negative calc operations", () => {
|
||||
const result = calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "-1d -2h 1m" });
|
||||
expect(result).toBe("2025-07-12T10:01:00.000Z");
|
||||
});
|
||||
|
||||
it('throws error for invalid unit', () => {
|
||||
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1x' })).toThrow(
|
||||
'Invalid date expression: +1x',
|
||||
it("throws error for invalid unit", () => {
|
||||
expect(() => calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1x" })).toThrow(
|
||||
"Invalid date expression: +1x",
|
||||
);
|
||||
});
|
||||
it('throws error for invalid unit weird', () => {
|
||||
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1&#^%' })).toThrow(
|
||||
'Invalid date expression: +1&#^%',
|
||||
it("throws error for invalid unit weird", () => {
|
||||
expect(() => calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1&#^%" })).toThrow(
|
||||
"Invalid date expression: +1&#^%",
|
||||
);
|
||||
});
|
||||
it('throws error for bad expression', () => {
|
||||
it("throws error for bad expression", () => {
|
||||
expect(() =>
|
||||
calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: 'bad expr' }),
|
||||
).toThrow('Invalid date expression: bad');
|
||||
calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "bad expr" }),
|
||||
).toThrow("Invalid date expression: bad");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-uuid",
|
||||
"displayName": "UUID Template Functions",
|
||||
"description": "Template functions for generating UUIDs",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for generating UUIDs",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { v1, v3, v4, v5, v6, v7 } from 'uuid';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import { v1, v3, v4, v5, v6, v7 } from "uuid";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'uuid.v1',
|
||||
description: 'Generate a UUID V1',
|
||||
name: "uuid.v1",
|
||||
description: "Generate a UUID V1",
|
||||
args: [],
|
||||
async onRender(): Promise<string | null> {
|
||||
return v1();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v3',
|
||||
description: 'Generate a UUID V3',
|
||||
name: "uuid.v3",
|
||||
description: "Generate a UUID V3",
|
||||
args: [
|
||||
{ type: 'text', name: 'name', label: 'Name' },
|
||||
{ type: "text", name: "name", label: "Name" },
|
||||
{
|
||||
type: 'text',
|
||||
name: 'namespace',
|
||||
label: 'Namespace UUID',
|
||||
description: 'A valid UUID to use as the namespace',
|
||||
placeholder: '24ced880-3bf4-11f0-8329-cd053d577f0e',
|
||||
type: "text",
|
||||
name: "namespace",
|
||||
label: "Namespace UUID",
|
||||
description: "A valid UUID to use as the namespace",
|
||||
placeholder: "24ced880-3bf4-11f0-8329-cd053d577f0e",
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
@@ -29,35 +29,35 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v4',
|
||||
description: 'Generate a UUID V4',
|
||||
name: "uuid.v4",
|
||||
description: "Generate a UUID V4",
|
||||
args: [],
|
||||
async onRender(): Promise<string | null> {
|
||||
return v4();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v5',
|
||||
description: 'Generate a UUID V5',
|
||||
name: "uuid.v5",
|
||||
description: "Generate a UUID V5",
|
||||
args: [
|
||||
{ type: 'text', name: 'name', label: 'Name' },
|
||||
{ type: 'text', name: 'namespace', label: 'Namespace' },
|
||||
{ type: "text", name: "name", label: "Name" },
|
||||
{ type: "text", name: "namespace", label: "Namespace" },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
return v5(String(args.values.name), String(args.values.namespace));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v6',
|
||||
description: 'Generate a UUID V6',
|
||||
name: "uuid.v6",
|
||||
description: "Generate a UUID V6",
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'timestamp',
|
||||
label: 'Timestamp',
|
||||
type: "text",
|
||||
name: "timestamp",
|
||||
label: "Timestamp",
|
||||
optional: true,
|
||||
description: 'Can be any format that can be parsed by JavaScript new Date(...)',
|
||||
placeholder: '2025-05-28T11:15:00Z',
|
||||
description: "Can be any format that can be parsed by JavaScript new Date(...)",
|
||||
placeholder: "2025-05-28T11:15:00Z",
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
@@ -65,8 +65,8 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uuid.v7',
|
||||
description: 'Generate a UUID V7',
|
||||
name: "uuid.v7",
|
||||
description: "Generate a UUID V7",
|
||||
args: [],
|
||||
async onRender(): Promise<string | null> {
|
||||
return v7();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@yaak/template-function-xml",
|
||||
"displayName": "XML Template Functions",
|
||||
"description": "Template functions for working with XML data",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with XML data",
|
||||
"main": "build/index.js",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
/* oxlint-disable no-base-to-string */
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import xpath from 'xpath';
|
||||
import { DOMParser } from "@xmldom/xmldom";
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import xpath from "xpath";
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
const RETURN_FIRST = "first";
|
||||
const RETURN_ALL = "all";
|
||||
const RETURN_JOIN = "join";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'xml.xpath',
|
||||
description: 'Filter XML-formatted text using XPath syntax',
|
||||
previewArgs: ['query'],
|
||||
name: "xml.xpath",
|
||||
description: "Filter XML-formatted text using XPath syntax",
|
||||
previewArgs: ["query"],
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
multiLine: true,
|
||||
placeholder: '<foo></foo>',
|
||||
placeholder: "<foo></foo>",
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'result',
|
||||
label: 'Return Format',
|
||||
type: "select",
|
||||
name: "result",
|
||||
label: "Return Format",
|
||||
defaultValue: RETURN_FIRST,
|
||||
options: [
|
||||
{ label: 'First result', value: RETURN_FIRST },
|
||||
{ label: 'All results', value: RETURN_ALL },
|
||||
{ label: 'Join with separator', value: RETURN_JOIN },
|
||||
{ label: "First result", value: RETURN_FIRST },
|
||||
{ label: "All results", value: RETURN_ALL },
|
||||
{ label: "Join with separator", value: RETURN_JOIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'join',
|
||||
type: 'text',
|
||||
label: 'Separator',
|
||||
name: "join",
|
||||
type: "text",
|
||||
label: "Separator",
|
||||
optional: true,
|
||||
defaultValue: ', ',
|
||||
defaultValue: ", ",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result !== RETURN_JOIN };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '//foo' },
|
||||
{ type: "text", name: "query", label: "Query", placeholder: "//foo" },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
@@ -62,7 +62,7 @@ export const plugin: PluginDefinition = {
|
||||
],
|
||||
};
|
||||
|
||||
export type XPathResult = 'first' | 'join' | 'all';
|
||||
export type XPathResult = "first" | "join" | "all";
|
||||
export function filterXPath(
|
||||
body: string,
|
||||
path: string,
|
||||
@@ -70,17 +70,17 @@ export function filterXPath(
|
||||
join: string | null,
|
||||
): string {
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const doc: any = new DOMParser().parseFromString(body, 'text/xml');
|
||||
const doc: any = new DOMParser().parseFromString(body, "text/xml");
|
||||
const items = xpath.select(path, doc, false);
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
return String(items);
|
||||
}
|
||||
if (!Array.isArray(items) || result === 'first') {
|
||||
return items[0] != null ? String(items[0].firstChild ?? '') : '';
|
||||
if (!Array.isArray(items) || result === "first") {
|
||||
return items[0] != null ? String(items[0].firstChild ?? "") : "";
|
||||
}
|
||||
if (result === 'join') {
|
||||
return items.map((item) => String(item.firstChild ?? '')).join(join ?? '');
|
||||
if (result === "join") {
|
||||
return items.map((item) => String(item.firstChild ?? "")).join(join ?? "");
|
||||
}
|
||||
// Not sure what cases this happens in (?)
|
||||
return String(items);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user