@toloka-platform/sdk 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli/toloka.js +152 -148
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/authoring.md +9 -1
- package/docs/cli.md +19 -6
- package/docs/getting-started.md +1 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +2 -2
- package/skills/catalog.json +6 -6
- package/skills/toloka-build/SKILL.md +5 -3
- package/skills/toloka-build/references/node-semantics.md +2 -2
- package/skills/toloka-build/references/workflow-and-cli.md +7 -5
- package/src/cli/auth.ts +51 -18
- package/src/cli/check.ts +5 -5
- package/src/cli/commands/auth.ts +5 -3
- package/src/cli/commands/project.ts +7 -2
- package/src/cli/commands/ui.ts +23 -0
- package/src/cli/config.ts +15 -9
- package/src/cli/context.ts +30 -12
- package/src/cli/credentials.ts +2 -2
- package/src/cli/environment.ts +5 -1
- package/src/cli/program.ts +5 -0
- package/src/cli/project-context.ts +5 -2
- package/src/cli/ui.ts +61 -0
- package/src/core-entity-types.json +32 -0
- package/src/testing/task-ui-browser.ts +45 -2
- package/src/testing/task-ui.ts +2 -9
- package/src/transport.ts +28 -6
- package/src/validate.ts +49 -2
package/src/cli/context.ts
CHANGED
|
@@ -10,8 +10,14 @@ import type { CliAnalytics } from "./analytics";
|
|
|
10
10
|
import type { BrowserLoginFlow } from "./browser-login";
|
|
11
11
|
import { createBrowserSessionAuth, findBrowserLogin } from "./browser-session";
|
|
12
12
|
import type { checkProject } from "./check";
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
13
|
+
import type { DescribeTaskUiComponents } from "../testing/task-ui-browser";
|
|
14
|
+
import {
|
|
15
|
+
PRODUCTION_BASE_URL,
|
|
16
|
+
resolveBaseUrl,
|
|
17
|
+
resolveConfiguredCredential,
|
|
18
|
+
resolveTolokaConfig,
|
|
19
|
+
} from "./config";
|
|
20
|
+
import type { CliCredential, ResolvedTolokaConfig, TolokaEnvironment } from "./config";
|
|
15
21
|
import { findCommandCredentials } from "./credentials";
|
|
16
22
|
import type { StoredCredentials } from "./credentials";
|
|
17
23
|
import { resolveProjectContext } from "./project-context";
|
|
@@ -36,6 +42,7 @@ export type ProgramContext = {
|
|
|
36
42
|
readonly browserLogin?: BrowserLoginFlow;
|
|
37
43
|
readonly runChecks?: typeof checkProject;
|
|
38
44
|
readonly analytics?: CliAnalytics;
|
|
45
|
+
readonly taskUiComponents?: DescribeTaskUiComponents;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
export type GlobalOptions = {
|
|
@@ -54,6 +61,7 @@ export type Resolved = {
|
|
|
54
61
|
readonly sourceRoot: string;
|
|
55
62
|
readonly client: PotatoClient;
|
|
56
63
|
readonly baseUrl: string;
|
|
64
|
+
readonly credential: CliCredential;
|
|
57
65
|
readonly state: TolokaState | undefined;
|
|
58
66
|
readonly stored: StoredCredentials | undefined;
|
|
59
67
|
readonly projectId?: string;
|
|
@@ -61,10 +69,11 @@ export type Resolved = {
|
|
|
61
69
|
|
|
62
70
|
export const CREDENTIAL_PRECEDENCE = [
|
|
63
71
|
"Credentials resolve in this order, highest first:",
|
|
64
|
-
" 1. --api-key
|
|
65
|
-
" 2.
|
|
66
|
-
" 3.
|
|
67
|
-
" 4. the
|
|
72
|
+
" 1. --api-key",
|
|
73
|
+
" 2. TOLOKA_SCOPED_TOKEN",
|
|
74
|
+
" 3. TOLOKA_API_KEY",
|
|
75
|
+
" 4. the API key stored by toloka login --api-key-stdin",
|
|
76
|
+
" 5. the browser login stored by toloka login",
|
|
68
77
|
`The platform defaults to ${PRODUCTION_BASE_URL}.`,
|
|
69
78
|
].join("\n");
|
|
70
79
|
|
|
@@ -205,7 +214,9 @@ export const resolveInitRoot = (globals: GlobalOptions): string =>
|
|
|
205
214
|
export const readCommandCredentials = async (globals: GlobalOptions, context: ProgramContext) => {
|
|
206
215
|
const { found, warning } = await findCommandCredentials(
|
|
207
216
|
context.environment,
|
|
208
|
-
globals.apiKey ??
|
|
217
|
+
globals.apiKey ??
|
|
218
|
+
context.environment.TOLOKA_SCOPED_TOKEN ??
|
|
219
|
+
context.environment.TOLOKA_API_KEY,
|
|
209
220
|
);
|
|
210
221
|
if (warning !== undefined) context.io.stderr(warning);
|
|
211
222
|
return found?.credentials;
|
|
@@ -229,7 +240,7 @@ export const resolveClient = async (
|
|
|
229
240
|
): Promise<{ readonly config: ResolvedTolokaConfig; readonly client: PotatoClient }> => {
|
|
230
241
|
const { environment } = context;
|
|
231
242
|
const browserLogin =
|
|
232
|
-
|
|
243
|
+
resolveConfiguredCredential(globals, environment, stored) === undefined
|
|
233
244
|
? (await findBrowserLogin(resolveBaseUrl(globals, environment, stored), environment))?.provider
|
|
234
245
|
: undefined;
|
|
235
246
|
const config = resolveTolokaConfig(globals, state, environment, stored, browserLogin);
|
|
@@ -239,10 +250,16 @@ export const resolveClient = async (
|
|
|
239
250
|
createClient(
|
|
240
251
|
credential.type === "apiKey"
|
|
241
252
|
? { baseUrl, apiKey: credential.apiKey }
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
: credential.type === "scopedToken"
|
|
254
|
+
? { baseUrl, scopedToken: credential.scopedToken }
|
|
255
|
+
: {
|
|
256
|
+
baseUrl,
|
|
257
|
+
accessToken: createBrowserSessionAuth({
|
|
258
|
+
provider: credential.provider,
|
|
259
|
+
baseUrl,
|
|
260
|
+
environment,
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
246
263
|
);
|
|
247
264
|
return { config, client };
|
|
248
265
|
};
|
|
@@ -261,6 +278,7 @@ export const resolveContext = async (
|
|
|
261
278
|
sourceRoot,
|
|
262
279
|
client,
|
|
263
280
|
baseUrl: config.baseUrl,
|
|
281
|
+
credential: config.credential,
|
|
264
282
|
state,
|
|
265
283
|
stored,
|
|
266
284
|
...(config.projectId === undefined ? {} : { projectId: config.projectId }),
|
package/src/cli/credentials.ts
CHANGED
|
@@ -192,12 +192,12 @@ export async function findStoredCredentials(
|
|
|
192
192
|
|
|
193
193
|
export async function findCommandCredentials(
|
|
194
194
|
environment: CliEnvironment,
|
|
195
|
-
|
|
195
|
+
overrideCredential: string | undefined,
|
|
196
196
|
): Promise<CommandCredentialLookup> {
|
|
197
197
|
try {
|
|
198
198
|
return { found: await findStoredCredentials(environment) };
|
|
199
199
|
} catch (error) {
|
|
200
|
-
if (!(error instanceof CredentialsError) ||
|
|
200
|
+
if (!(error instanceof CredentialsError) || overrideCredential === undefined) throw error;
|
|
201
201
|
return { found: undefined, warning: error.message };
|
|
202
202
|
}
|
|
203
203
|
}
|
package/src/cli/environment.ts
CHANGED
|
@@ -6,7 +6,11 @@ const LEGACY_ENVIRONMENT_NAMES: Readonly<Record<string, string>> = {
|
|
|
6
6
|
POTATO_PROJECT_ID: "TOLOKA_PROJECT_ID",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const CREDENTIAL_ENVIRONMENT_NAMES = [
|
|
10
|
+
"TOLOKA_SCOPED_TOKEN",
|
|
11
|
+
"TOLOKA_API_KEY",
|
|
12
|
+
"POTATO_API_KEY",
|
|
13
|
+
];
|
|
10
14
|
|
|
11
15
|
export type NormalizedEnvironment = {
|
|
12
16
|
readonly environment: TolokaEnvironment;
|
package/src/cli/program.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
|
|
|
5
5
|
import type { AutoUpdateHooks } from "./auto-update";
|
|
6
6
|
import { maybeAutoUpdate } from "./auto-update";
|
|
7
7
|
import type { BrowserLoginFlow } from "./browser-login";
|
|
8
|
+
import type { DescribeTaskUiComponents } from "../testing/task-ui-browser";
|
|
8
9
|
import { CHECK_RESULT_FILE_ENV, type checkProject } from "./check";
|
|
9
10
|
import { registerAudienceCommands } from "./commands/audience";
|
|
10
11
|
import { registerAuthCommands } from "./commands/auth";
|
|
@@ -20,6 +21,7 @@ import { registerSkillCommands } from "./commands/skill";
|
|
|
20
21
|
import { registerSourceCommands } from "./commands/source";
|
|
21
22
|
import { registerStatusCommand } from "./commands/status";
|
|
22
23
|
import { registerTrainingCommands } from "./commands/training";
|
|
24
|
+
import { registerUiCommands } from "./commands/ui";
|
|
23
25
|
import { registerVersionCommand } from "./commands/version";
|
|
24
26
|
import type { TolokaEnvironment } from "./config";
|
|
25
27
|
import {
|
|
@@ -103,6 +105,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
103
105
|
registerStatusCommand(program, host);
|
|
104
106
|
registerReportCommand(program, host);
|
|
105
107
|
registerSkillCommands(program, host);
|
|
108
|
+
registerUiCommands(program, host);
|
|
106
109
|
|
|
107
110
|
return { program, exitCode };
|
|
108
111
|
}
|
|
@@ -198,6 +201,7 @@ export async function runTolokaCli(
|
|
|
198
201
|
runChecks?: typeof checkProject,
|
|
199
202
|
autoUpdate?: AutoUpdateHooks,
|
|
200
203
|
browserLogin?: BrowserLoginFlow,
|
|
204
|
+
taskUiComponents?: DescribeTaskUiComponents,
|
|
201
205
|
): Promise<number> {
|
|
202
206
|
const { environment, warnings } = normalizeEnvironment(rawEnvironment);
|
|
203
207
|
for (const warning of warnings) io.stderr(warning);
|
|
@@ -220,6 +224,7 @@ export async function runTolokaCli(
|
|
|
220
224
|
...(injectedClient === undefined ? {} : { injectedClient }),
|
|
221
225
|
...(browserLogin === undefined ? {} : { browserLogin }),
|
|
222
226
|
...(runChecks === undefined ? {} : { runChecks }),
|
|
227
|
+
...(taskUiComponents === undefined ? {} : { taskUiComponents }),
|
|
223
228
|
};
|
|
224
229
|
const { program, exitCode } = createProgram(baseContext);
|
|
225
230
|
const startedAt = Date.now();
|
|
@@ -50,7 +50,7 @@ export const NO_PROJECT_SELECTED_MESSAGE =
|
|
|
50
50
|
"No project selected. Run toloka project create for a new project, toloka project add for an existing project, or cd into a checkout.";
|
|
51
51
|
|
|
52
52
|
const NETWORK_MISMATCH_MESSAGE =
|
|
53
|
-
"project add writes the machine registry; unset --api-key / --base-url / TOLOKA_API_KEY / TOLOKA_BASE_URL, or run login against that platform first.";
|
|
53
|
+
"project add writes the machine registry; unset --api-key / --base-url / TOLOKA_SCOPED_TOKEN / TOLOKA_API_KEY / TOLOKA_BASE_URL, or run login against that platform first.";
|
|
54
54
|
|
|
55
55
|
const ALIAS_PATTERN = /^[^/\s]+$/u;
|
|
56
56
|
|
|
@@ -187,7 +187,10 @@ const assertStoredNetworkMatches = (
|
|
|
187
187
|
environment: TolokaEnvironment,
|
|
188
188
|
stored: StoredCredentials,
|
|
189
189
|
) => {
|
|
190
|
-
const flagOrEnvKey =
|
|
190
|
+
const flagOrEnvKey =
|
|
191
|
+
firstNonEmpty(flags.apiKey) ??
|
|
192
|
+
firstNonEmpty(environment.TOLOKA_SCOPED_TOKEN) ??
|
|
193
|
+
firstNonEmpty(environment.TOLOKA_API_KEY);
|
|
191
194
|
const flagOrEnvBase =
|
|
192
195
|
firstNonEmpty(flags.baseUrl) ??
|
|
193
196
|
firstNonEmpty(environment.TOLOKA_BASE_URL) ??
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { TaskUiComponentDescription } from "../testing/types";
|
|
2
|
+
import type { DescribeTaskUiComponents } from "../testing/task-ui-browser";
|
|
3
|
+
import type { CliIo } from "./context";
|
|
4
|
+
|
|
5
|
+
const formatList = (title: string, items: readonly string[]): string[] =>
|
|
6
|
+
items.length === 0 ? [] : [`${title}:`, ...items.map((item) => `- ${item}`)];
|
|
7
|
+
|
|
8
|
+
export const formatComponentIndex = (
|
|
9
|
+
components: Readonly<Record<string, TaskUiComponentDescription>>,
|
|
10
|
+
): string =>
|
|
11
|
+
[
|
|
12
|
+
...Object.values(components).map(({ name, description }) => `${name}: ${description}`),
|
|
13
|
+
"",
|
|
14
|
+
"Run `toloka ui components <name> [name...]` for the props, value shape, and example of a component.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
export const formatComponentDetails = (
|
|
18
|
+
components: Readonly<Record<string, TaskUiComponentDescription>>,
|
|
19
|
+
): string =>
|
|
20
|
+
Object.values(components)
|
|
21
|
+
.map((component) =>
|
|
22
|
+
[
|
|
23
|
+
`## ${component.name}`,
|
|
24
|
+
component.description,
|
|
25
|
+
`When to use: ${component.whenToUse}`,
|
|
26
|
+
...formatList("Capabilities", component.capabilities),
|
|
27
|
+
...formatList("Limitations", component.limitations),
|
|
28
|
+
...formatList("Best practices", component.bestPractices),
|
|
29
|
+
"Props:",
|
|
30
|
+
component.propsType,
|
|
31
|
+
...(component.valueType === undefined ? [] : ["Value:", component.valueType]),
|
|
32
|
+
...(component.example === undefined
|
|
33
|
+
? []
|
|
34
|
+
: ["Example:", component.example.jsxTemplate, "Example output:", component.example.output]),
|
|
35
|
+
].join("\n"),
|
|
36
|
+
)
|
|
37
|
+
.join("\n\n");
|
|
38
|
+
|
|
39
|
+
export const uiComponentsCommand = async ({
|
|
40
|
+
sourceRoot,
|
|
41
|
+
names,
|
|
42
|
+
json,
|
|
43
|
+
io,
|
|
44
|
+
describe,
|
|
45
|
+
}: {
|
|
46
|
+
sourceRoot: string;
|
|
47
|
+
names: readonly string[];
|
|
48
|
+
json: boolean;
|
|
49
|
+
io: CliIo;
|
|
50
|
+
describe: DescribeTaskUiComponents;
|
|
51
|
+
}): Promise<void> => {
|
|
52
|
+
const components = await describe({
|
|
53
|
+
resolveFrom: sourceRoot,
|
|
54
|
+
...(names.length === 0 ? {} : { names: [...names] }),
|
|
55
|
+
});
|
|
56
|
+
if (json) {
|
|
57
|
+
io.stdout(JSON.stringify(components, undefined, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
io.stdout(names.length === 0 ? formatComponentIndex(components) : formatComponentDetails(components));
|
|
61
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
"JUDGEMENT",
|
|
3
|
+
"EXPLANATION",
|
|
4
|
+
"PROMPT",
|
|
5
|
+
"USER_PROMPT",
|
|
6
|
+
"RESPONSE",
|
|
7
|
+
"RESPONSE_EVALUATION",
|
|
8
|
+
"RUBRIC",
|
|
9
|
+
"RUBRICS_GROUP",
|
|
10
|
+
"DIALOG",
|
|
11
|
+
"DIALOG_STEP",
|
|
12
|
+
"DIALOG_EVALUATION",
|
|
13
|
+
"SHORT_ANSWER",
|
|
14
|
+
"FEEDBACK",
|
|
15
|
+
"CONTEXT",
|
|
16
|
+
"IMAGE",
|
|
17
|
+
"ANNOTATION",
|
|
18
|
+
"SINGLE_LABEL",
|
|
19
|
+
"MULTI_LABEL",
|
|
20
|
+
"VIDEO",
|
|
21
|
+
"DOCUMENT",
|
|
22
|
+
"GOLDEN_SET",
|
|
23
|
+
"LOGS",
|
|
24
|
+
"SOLUTION",
|
|
25
|
+
"CODE",
|
|
26
|
+
"METADATA",
|
|
27
|
+
"AUDIO",
|
|
28
|
+
"AGENT_TRAJECTORY",
|
|
29
|
+
"MCP_SERVER",
|
|
30
|
+
"CONTAINER",
|
|
31
|
+
"CUSTOM"
|
|
32
|
+
]
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { assert } from "es-toolkit";
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import type * as Playwright from "playwright";
|
|
6
|
+
import type { Browser, Page } from "playwright";
|
|
3
7
|
import {
|
|
4
8
|
TaskUiInfrastructureError,
|
|
5
9
|
type TaskUiComponentDescription,
|
|
@@ -95,8 +99,20 @@ export const readDataPath = (state: Record<string, unknown>): string | undefined
|
|
|
95
99
|
export const componentIds = (tree: string): string[] =>
|
|
96
100
|
[...tree.matchAll(/\bid="([^"]+)"/gu)].map((match) => match[1]!);
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
const playwrightSpecifier = (resolveFrom: string | undefined): string => {
|
|
103
|
+
if (resolveFrom === undefined) return "playwright";
|
|
99
104
|
try {
|
|
105
|
+
return pathToFileURL(createRequire(join(resolveFrom, "package.json")).resolve("playwright")).href;
|
|
106
|
+
} catch {
|
|
107
|
+
return "playwright";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const launchDefaultBrowser = async ({
|
|
112
|
+
resolveFrom,
|
|
113
|
+
}: { resolveFrom?: string } = {}): Promise<Browser> => {
|
|
114
|
+
try {
|
|
115
|
+
const { chromium }: typeof Playwright = await import(playwrightSpecifier(resolveFrom));
|
|
100
116
|
return await chromium.launch({ headless: true });
|
|
101
117
|
} catch (error) {
|
|
102
118
|
throw new TaskUiInfrastructureError(
|
|
@@ -117,4 +133,31 @@ export const waitForTaskUiApi = async (page: Page): Promise<void> => {
|
|
|
117
133
|
);
|
|
118
134
|
};
|
|
119
135
|
|
|
136
|
+
export const readComponentDocumentation = async (
|
|
137
|
+
page: Page,
|
|
138
|
+
launcherUrl: string,
|
|
139
|
+
names?: string[],
|
|
140
|
+
): Promise<Record<string, TaskUiComponentDescription>> => {
|
|
141
|
+
await page.goto(launcherUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
142
|
+
await waitForTaskUiApi(page);
|
|
143
|
+
return selectComponentDocumentation(
|
|
144
|
+
await evaluateWithApi(page, { operation: "getComponentsDescriptions" }),
|
|
145
|
+
names,
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type DescribeTaskUiComponents = (input: {
|
|
150
|
+
names?: string[];
|
|
151
|
+
resolveFrom?: string;
|
|
152
|
+
}) => Promise<Record<string, TaskUiComponentDescription>>;
|
|
153
|
+
|
|
154
|
+
export const describeTaskUiComponents: DescribeTaskUiComponents = async ({ names, resolveFrom }) => {
|
|
155
|
+
const browser = await launchDefaultBrowser({ resolveFrom });
|
|
156
|
+
try {
|
|
157
|
+
return await readComponentDocumentation(await browser.newPage(), DEFAULT_TBX_LAUNCHER_URL, names);
|
|
158
|
+
} finally {
|
|
159
|
+
await browser.close();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
120
163
|
export type { Browser, Page, TbxTemplate };
|
package/src/testing/task-ui.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
componentIds,
|
|
10
10
|
evaluateWithApi,
|
|
11
11
|
launchDefaultBrowser,
|
|
12
|
+
readComponentDocumentation,
|
|
12
13
|
readDataPath,
|
|
13
14
|
selectComponentDocumentation,
|
|
14
15
|
waitForTaskUiApi,
|
|
@@ -287,15 +288,7 @@ const createTaskUiTest = ({
|
|
|
287
288
|
const context = await _uiBrowser.newContext();
|
|
288
289
|
const page = await context.newPage();
|
|
289
290
|
try {
|
|
290
|
-
await page
|
|
291
|
-
waitUntil: "domcontentloaded",
|
|
292
|
-
timeout: 30_000,
|
|
293
|
-
});
|
|
294
|
-
await waitForTaskUiApi(page);
|
|
295
|
-
return selectComponentDocumentation(
|
|
296
|
-
await evaluateWithApi(page, { operation: "getComponentsDescriptions" }),
|
|
297
|
-
names,
|
|
298
|
-
);
|
|
291
|
+
return await readComponentDocumentation(page, _uiConfig.launcherUrl, names);
|
|
299
292
|
} finally {
|
|
300
293
|
await page.close();
|
|
301
294
|
await context.close();
|
package/src/transport.ts
CHANGED
|
@@ -2,12 +2,17 @@ import { assert } from "es-toolkit";
|
|
|
2
2
|
import { PotatoApiError } from "./errors";
|
|
3
3
|
|
|
4
4
|
const BASE_URL_HEADER = "x-potato-sdk-base-url";
|
|
5
|
-
const SCOPED_TOKEN_PREFIX = "pst_";
|
|
5
|
+
export const SCOPED_TOKEN_PREFIX = "pst_";
|
|
6
6
|
const SCOPED_TOKEN_HEADER = "X-Scoped-Token";
|
|
7
7
|
|
|
8
8
|
export type ClientCredential =
|
|
9
|
-
| { readonly apiKey: string; readonly accessToken?: undefined }
|
|
10
|
-
| {
|
|
9
|
+
| { readonly apiKey: string; readonly accessToken?: undefined; readonly scopedToken?: undefined }
|
|
10
|
+
| {
|
|
11
|
+
readonly accessToken: () => Promise<string>;
|
|
12
|
+
readonly apiKey?: undefined;
|
|
13
|
+
readonly scopedToken?: undefined;
|
|
14
|
+
}
|
|
15
|
+
| { readonly scopedToken: string; readonly accessToken?: undefined; readonly apiKey?: undefined };
|
|
11
16
|
|
|
12
17
|
type TransportRequestInit = RequestInit & {
|
|
13
18
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -29,6 +34,13 @@ export const createRequestOptions = (
|
|
|
29
34
|
const accessToken = credential.accessToken;
|
|
30
35
|
return { headers, fetch, authorization: async () => `Bearer ${await accessToken()}` };
|
|
31
36
|
}
|
|
37
|
+
if (credential.scopedToken !== undefined) {
|
|
38
|
+
assert(credential.scopedToken.length > 0, "The SDK scoped token cannot be empty");
|
|
39
|
+
return {
|
|
40
|
+
headers: { ...headers, [SCOPED_TOKEN_HEADER]: credential.scopedToken },
|
|
41
|
+
fetch,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
32
44
|
|
|
33
45
|
const { apiKey } = credential;
|
|
34
46
|
assert(apiKey.length > 0, "The SDK API key cannot be empty");
|
|
@@ -51,14 +63,24 @@ const prepareRequestInit = (requestOptions: RequestInit, headers: Headers): Requ
|
|
|
51
63
|
// those against the base's origin, dropping its path. Join them instead, so a base URL served under a
|
|
52
64
|
// path prefix (a gateway in front of the platform) keeps that prefix.
|
|
53
65
|
const resolveRequestUrl = (url: string, baseUrl: string): URL => {
|
|
54
|
-
if (!url.startsWith("/") || url.startsWith("//")) return new URL(url, baseUrl);
|
|
55
66
|
const base = new URL(baseUrl);
|
|
56
|
-
|
|
67
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
68
|
+
const suffix = url.startsWith("/") ? url : `/${url}`;
|
|
69
|
+
const resolved = new URL(`${base.origin}${basePath}${suffix}`);
|
|
70
|
+
assert(
|
|
71
|
+
resolved.origin === base.origin,
|
|
72
|
+
"The SDK refused a request whose origin did not match the base URL",
|
|
73
|
+
);
|
|
74
|
+
return resolved;
|
|
57
75
|
};
|
|
58
76
|
|
|
59
77
|
export function potatoFetch<T>(url: string, options: TransportRequestInit): Promise<T>;
|
|
60
78
|
export async function potatoFetch(url: string, options: TransportRequestInit): Promise<unknown> {
|
|
61
|
-
const {
|
|
79
|
+
const {
|
|
80
|
+
fetch: fetchImplementation = globalThis.fetch,
|
|
81
|
+
authorization,
|
|
82
|
+
...requestOptions
|
|
83
|
+
} = options;
|
|
62
84
|
const headers = new Headers(requestOptions.headers);
|
|
63
85
|
const baseUrl = headers.get(BASE_URL_HEADER);
|
|
64
86
|
assert(baseUrl, "The SDK base URL is missing");
|
package/src/validate.ts
CHANGED
|
@@ -2,12 +2,55 @@ import Ajv2020, { type ErrorObject } from 'ajv/dist/2020.js';
|
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
import { assert } from 'es-toolkit';
|
|
4
4
|
import type { PipelineGraphV1 } from '../openapi/gen/models';
|
|
5
|
+
import coreEntityTypes from './core-entity-types.json' with { type: 'json' };
|
|
5
6
|
import { PipelineGraphValidationError, type PipelineGraphValidationIssue } from './errors';
|
|
6
7
|
import pipelineGraphV1Contract from './pipeline-graph-v1.contract.json' with { type: 'json' };
|
|
7
8
|
|
|
8
9
|
const ajv = new Ajv2020({ strict: false });
|
|
9
10
|
addFormats(ajv);
|
|
10
|
-
const validatePipelineGraph = ajv.compile(pipelineGraphV1Contract);
|
|
11
|
+
const validatePipelineGraph = ajv.compile<PipelineGraphV1>(pipelineGraphV1Contract);
|
|
12
|
+
|
|
13
|
+
const CORE_ENTITY_TYPES = new Set<string>(coreEntityTypes);
|
|
14
|
+
const LABEL_ENTITY_TYPES = new Set(['SINGLE_LABEL', 'MULTI_LABEL']);
|
|
15
|
+
const LABEL_VALUE_PATTERN = /^[a-z0-9]+(_[a-z0-9]+)*$/;
|
|
16
|
+
|
|
17
|
+
type EntityLike = { entityType: string; allowedLabels?: { label: string; value: string }[] };
|
|
18
|
+
|
|
19
|
+
const entityIssues = (
|
|
20
|
+
entities: readonly EntityLike[],
|
|
21
|
+
side: 'input' | 'output',
|
|
22
|
+
prefix: string,
|
|
23
|
+
): PipelineGraphValidationIssue[] =>
|
|
24
|
+
entities.flatMap((entity, index) => {
|
|
25
|
+
const path = `${prefix}/${side}/${index}`;
|
|
26
|
+
const issues: PipelineGraphValidationIssue[] = [];
|
|
27
|
+
if (!CORE_ENTITY_TYPES.has(entity.entityType)) {
|
|
28
|
+
issues.push({
|
|
29
|
+
path: `${path}/entityType`,
|
|
30
|
+
reason: `must be one of the supported core entity types: ${coreEntityTypes.join(', ')}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (side === 'output' && LABEL_ENTITY_TYPES.has(entity.entityType)) {
|
|
34
|
+
const labels = entity.allowedLabels ?? [];
|
|
35
|
+
if (labels.length < 2) {
|
|
36
|
+
issues.push({ path: `${path}/allowedLabels`, reason: 'label entities require at least 2 allowedLabels' });
|
|
37
|
+
}
|
|
38
|
+
if (labels.some(({ value }) => !LABEL_VALUE_PATTERN.test(value))) {
|
|
39
|
+
issues.push({ path: `${path}/allowedLabels`, reason: 'label values must be snake_case (e.g. this_is_value)' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const graphEntityIssues = (graph: PipelineGraphV1): PipelineGraphValidationIssue[] =>
|
|
46
|
+
graph.nodes.flatMap((node, index) => {
|
|
47
|
+
if (node.type !== 'generation' && node.type !== 'quorum') return [];
|
|
48
|
+
const prefix = `/nodes/${index}/settings/entities`;
|
|
49
|
+
return [
|
|
50
|
+
...entityIssues(node.settings.entities.input, 'input', prefix),
|
|
51
|
+
...entityIssues(node.settings.entities.output, 'output', prefix),
|
|
52
|
+
];
|
|
53
|
+
});
|
|
11
54
|
|
|
12
55
|
const toValidationIssue = (error: ErrorObject): PipelineGraphValidationIssue => {
|
|
13
56
|
const property =
|
|
@@ -25,7 +68,11 @@ const toValidationIssue = (error: ErrorObject): PipelineGraphValidationIssue =>
|
|
|
25
68
|
};
|
|
26
69
|
|
|
27
70
|
export function validate(graph: unknown): asserts graph is PipelineGraphV1 {
|
|
28
|
-
if (validatePipelineGraph(graph))
|
|
71
|
+
if (validatePipelineGraph(graph)) {
|
|
72
|
+
const issues = graphEntityIssues(graph);
|
|
73
|
+
if (issues.length > 0) throw PipelineGraphValidationError.fromIssues(issues);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
29
76
|
|
|
30
77
|
const errors = validatePipelineGraph.errors;
|
|
31
78
|
assert(errors, 'Pipeline graph validation failed without errors');
|