@work-shape/catalyst-api-client 0.8.0-rc.1 → 0.8.0-rc.11
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/dist/index.d.ts +83 -5
- package/dist/index.js +117 -9
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ApiErrorCode, Operation, OperationPathParamName, OperationRequestHeader, apiOperations, ApiOperationName } from '@work-shape/catalyst-api-contract';
|
|
2
|
+
import { ApiErrorCode, LocaleCode, Operation, OperationPathParamName, OperationRequestHeader, apiOperations, ApiOperationName } from '@work-shape/catalyst-api-contract';
|
|
3
3
|
export * from '@work-shape/catalyst-api-contract';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -22,14 +22,64 @@ declare const authSessionSchema: z.ZodNullable<z.ZodObject<{
|
|
|
22
22
|
type AuthSession = NonNullable<z.infer<typeof authSessionSchema>>;
|
|
23
23
|
interface AuthResult {
|
|
24
24
|
ok: boolean;
|
|
25
|
+
/** The sign-in library's code for a refusal, such as `INVALID_EMAIL_OR_PASSWORD`. */
|
|
26
|
+
code?: string;
|
|
25
27
|
message?: string;
|
|
28
|
+
/** The HTTP status of a refusal; 429 is the instance's limit on attempts. */
|
|
29
|
+
status?: number;
|
|
30
|
+
}
|
|
31
|
+
/** The second factor of a sign-in: a code from the authenticator app, or a backup code. */
|
|
32
|
+
type SecondFactorKind = "app" | "backup";
|
|
33
|
+
/** A sign-in: refused, signed in, or waiting for the second factor (TWO-FACTOR-1). */
|
|
34
|
+
interface SignInResult extends AuthResult {
|
|
35
|
+
/** The password was right and the account asks for a code before there is a session. */
|
|
36
|
+
secondFactor?: true;
|
|
37
|
+
}
|
|
38
|
+
/** What an authenticator app reads, and the ten backup codes, shown once. */
|
|
39
|
+
interface TwoFactorSetup {
|
|
40
|
+
totpUri: string;
|
|
41
|
+
backupCodes: string[];
|
|
26
42
|
}
|
|
27
43
|
declare function getAuthSession(apiBaseUrl: string): Promise<AuthSession | null>;
|
|
28
44
|
declare function signInWithEmail(input: {
|
|
29
45
|
apiBaseUrl: string;
|
|
30
46
|
email: string;
|
|
31
47
|
password: string;
|
|
48
|
+
}): Promise<SignInResult>;
|
|
49
|
+
/**
|
|
50
|
+
* The second sign-in step: a code from the authenticator app, or a backup code, which signs in
|
|
51
|
+
* once. `trustBrowser` skips the code in this browser for 30 days from now.
|
|
52
|
+
*/
|
|
53
|
+
declare function verifySecondFactor(input: {
|
|
54
|
+
apiBaseUrl: string;
|
|
55
|
+
code: string;
|
|
56
|
+
kind: SecondFactorKind;
|
|
57
|
+
trustBrowser?: boolean;
|
|
58
|
+
}): Promise<AuthResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Starts the setup of two-factor sign-in with the password. The factor is on only after the
|
|
61
|
+
* first code from the app is confirmed with `verifySecondFactor`.
|
|
62
|
+
*/
|
|
63
|
+
declare function startTwoFactorSetup(input: {
|
|
64
|
+
apiBaseUrl: string;
|
|
65
|
+
password: string;
|
|
66
|
+
}): Promise<AuthResult & {
|
|
67
|
+
setup?: TwoFactorSetup;
|
|
68
|
+
}>;
|
|
69
|
+
/** Turns two-factor sign-in off with the password; every trusted browser is forgotten. */
|
|
70
|
+
declare function turnOffTwoFactor(input: {
|
|
71
|
+
apiBaseUrl: string;
|
|
72
|
+
password: string;
|
|
32
73
|
}): Promise<AuthResult>;
|
|
74
|
+
/** Replaces all ten backup codes with the password. The new ones are shown once. */
|
|
75
|
+
declare function regenerateBackupCodes(input: {
|
|
76
|
+
apiBaseUrl: string;
|
|
77
|
+
password: string;
|
|
78
|
+
}): Promise<AuthResult & {
|
|
79
|
+
backupCodes?: string[];
|
|
80
|
+
}>;
|
|
81
|
+
/** Every browser of the person asks for the code again at its next sign-in. */
|
|
82
|
+
declare function forgetTrustedBrowsers(apiBaseUrl: string): Promise<AuthResult>;
|
|
33
83
|
/**
|
|
34
84
|
* For a tool outside a browser, which keeps no cookies: signs in and returns a fetch that
|
|
35
85
|
* carries the session cookie and the origin the instance trusts on every request.
|
|
@@ -82,6 +132,13 @@ interface ApiClientOptions {
|
|
|
82
132
|
*/
|
|
83
133
|
getToken?: () => string | undefined | Promise<string | undefined>;
|
|
84
134
|
fetchImpl?: typeof fetch;
|
|
135
|
+
/** Cancels this client's requests when its owning session ends. */
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
/**
|
|
138
|
+
* The language every request asks the server to answer in, read at each request. A browser's
|
|
139
|
+
* own Accept-Language names its settings, not the language a person chose in the interface.
|
|
140
|
+
*/
|
|
141
|
+
locale?: LocaleCode;
|
|
85
142
|
/** Only available without a token source; browser navigation cannot send a bearer token. */
|
|
86
143
|
browserManagedDownloads?: boolean;
|
|
87
144
|
/**
|
|
@@ -127,7 +184,12 @@ type ControlPart<Op extends Operation> = {
|
|
|
127
184
|
} ? {
|
|
128
185
|
/** Called when the server has nothing after the given position and sends no stream. */
|
|
129
186
|
onCaughtUp?: () => void;
|
|
130
|
-
}
|
|
187
|
+
} & (Op["response"] extends {
|
|
188
|
+
skip: infer Skip extends z.ZodType;
|
|
189
|
+
} ? {
|
|
190
|
+
/** Called for an event this client does not know, which the stream passes over. */
|
|
191
|
+
onSkipped?: (value: z.output<Skip>) => void;
|
|
192
|
+
} : unknown) : unknown);
|
|
131
193
|
/**
|
|
132
194
|
* An operation that takes an `Idempotency-Key` gets a new one with every call. A caller that
|
|
133
195
|
* sends the same call again after an answer that never arrived passes the key of the first.
|
|
@@ -139,7 +201,18 @@ type IdempotencyPart<Op> = Op extends {
|
|
|
139
201
|
} ? "Idempotency-Key" extends Header ? {
|
|
140
202
|
idempotencyKey?: string;
|
|
141
203
|
} : unknown : unknown;
|
|
142
|
-
|
|
204
|
+
/**
|
|
205
|
+
* An operation that takes `Catalyst-Unattended` is sent with it when `unattended` is set: nobody
|
|
206
|
+
* watches the caller's terminal, so a call that needs a confirmation waits in the Inbox.
|
|
207
|
+
*/
|
|
208
|
+
type UnattendedPart<Op> = Op extends {
|
|
209
|
+
headers: {
|
|
210
|
+
request: readonly (infer Header extends OperationRequestHeader)[];
|
|
211
|
+
};
|
|
212
|
+
} ? "Catalyst-Unattended" extends Header ? {
|
|
213
|
+
unattended?: boolean;
|
|
214
|
+
} : unknown : unknown;
|
|
215
|
+
type InputOf<Op extends Operation> = ParamsPart<Op["path"]> & QueryPart<Op> & BodyPart<Op> & IdempotencyPart<Op> & UnattendedPart<Op> & ControlPart<Op>;
|
|
143
216
|
/**
|
|
144
217
|
* How a call of an operation that may wait for an approval came out. A refusal and a failure
|
|
145
218
|
* are thrown as `ApiError`, as for every operation.
|
|
@@ -153,7 +226,10 @@ type OperationOutcome<Output, Run = unknown> = {
|
|
|
153
226
|
status: "done";
|
|
154
227
|
output: Output;
|
|
155
228
|
}
|
|
156
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* Nothing ran yet: somebody decides it, an approver or, for a run `pending_confirmation`, the
|
|
231
|
+
* caller in their Inbox. `location` is where the run is read again.
|
|
232
|
+
*/
|
|
157
233
|
| {
|
|
158
234
|
status: "pending_approval";
|
|
159
235
|
run: Run;
|
|
@@ -169,6 +245,8 @@ type OutputOf<Op extends Operation> = Op["response"] extends {
|
|
|
169
245
|
kind: "sse";
|
|
170
246
|
schema: infer Schema extends z.ZodType;
|
|
171
247
|
} ? AsyncIterable<z.output<Schema>> : Op["response"] extends {
|
|
248
|
+
kind: "redirect";
|
|
249
|
+
} ? Promise<undefined> : Op["response"] extends {
|
|
172
250
|
schema: infer Schema extends z.ZodType;
|
|
173
251
|
} ? Op extends {
|
|
174
252
|
accepted: infer Accepted extends z.ZodType;
|
|
@@ -218,4 +296,4 @@ declare function listAll<Item>(readPage: (paging: {
|
|
|
218
296
|
nextCursor?: string;
|
|
219
297
|
}>): Promise<Item[]>;
|
|
220
298
|
|
|
221
|
-
export { AUTH_MOUNT_PATH, type ApiClient, type ApiClientFor, type ApiClientOptions, ApiError, type ApiOperationMethods, ApiResponseShapeError, type AuthResult, type AuthSession, type OperationInput, type OperationOutcome, completedOutput, createApiClient, createApiClientFor, getAuthSession, listAll, refusalError, signInForSessionFetch, signInWithEmail, signOut, transportError, unreadableResponseError };
|
|
299
|
+
export { AUTH_MOUNT_PATH, type ApiClient, type ApiClientFor, type ApiClientOptions, ApiError, type ApiOperationMethods, ApiResponseShapeError, type AuthResult, type AuthSession, type OperationInput, type OperationOutcome, type SecondFactorKind, type SignInResult, type TwoFactorSetup, completedOutput, createApiClient, createApiClientFor, forgetTrustedBrowsers, getAuthSession, listAll, refusalError, regenerateBackupCodes, signInForSessionFetch, signInWithEmail, signOut, startTwoFactorSetup, transportError, turnOffTwoFactor, unreadableResponseError, verifySecondFactor };
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,15 @@ var authSessionSchema = z.object({
|
|
|
6
6
|
user: z.object({ id: z.string(), email: z.string(), name: z.string() })
|
|
7
7
|
}).nullable();
|
|
8
8
|
var authFailureSchema = z.object({
|
|
9
|
+
code: z.string().optional(),
|
|
9
10
|
message: z.string().optional(),
|
|
10
11
|
error: z.object({ message: z.string().optional() }).optional()
|
|
11
12
|
});
|
|
13
|
+
var signInAnswerSchema = z.object({ twoFactorRedirect: z.boolean().optional() });
|
|
14
|
+
var twoFactorSetupSchema = z.object({
|
|
15
|
+
totpURI: z.string(),
|
|
16
|
+
backupCodes: z.array(z.string())
|
|
17
|
+
});
|
|
12
18
|
async function getAuthSession(apiBaseUrl) {
|
|
13
19
|
const response = await fetch(authUrl(apiBaseUrl, "/get-session"), {
|
|
14
20
|
credentials: "include"
|
|
@@ -32,12 +38,64 @@ async function signInWithEmail(input) {
|
|
|
32
38
|
})
|
|
33
39
|
});
|
|
34
40
|
if (response.ok) {
|
|
35
|
-
|
|
41
|
+
const answer = signInAnswerSchema.safeParse(await response.json().catch(() => void 0));
|
|
42
|
+
return answer.data?.twoFactorRedirect === true ? { ok: true, secondFactor: true } : { ok: true };
|
|
43
|
+
}
|
|
44
|
+
return failureOf(response, "Sign in failed");
|
|
45
|
+
}
|
|
46
|
+
async function verifySecondFactor(input) {
|
|
47
|
+
const path = input.kind === "app" ? "/two-factor/verify-totp" : "/two-factor/verify-backup-code";
|
|
48
|
+
const response = await postAuth(input.apiBaseUrl, path, {
|
|
49
|
+
code: input.code.replaceAll(/\s/gu, ""),
|
|
50
|
+
trustDevice: input.trustBrowser === true
|
|
51
|
+
});
|
|
52
|
+
return response.ok ? { ok: true } : failureOf(response, "The code was not accepted");
|
|
53
|
+
}
|
|
54
|
+
async function startTwoFactorSetup(input) {
|
|
55
|
+
const response = await postAuth(input.apiBaseUrl, "/two-factor/enable", {
|
|
56
|
+
password: input.password
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return failureOf(response, "Two-factor sign-in could not be set up");
|
|
60
|
+
}
|
|
61
|
+
const setup = twoFactorSetupSchema.parse(await response.json());
|
|
62
|
+
return { ok: true, setup: { totpUri: setup.totpURI, backupCodes: setup.backupCodes } };
|
|
63
|
+
}
|
|
64
|
+
async function turnOffTwoFactor(input) {
|
|
65
|
+
const response = await postAuth(input.apiBaseUrl, "/two-factor/disable", {
|
|
66
|
+
password: input.password
|
|
67
|
+
});
|
|
68
|
+
return response.ok ? { ok: true } : failureOf(response, "Two-factor sign-in stayed on");
|
|
69
|
+
}
|
|
70
|
+
async function regenerateBackupCodes(input) {
|
|
71
|
+
const response = await postAuth(input.apiBaseUrl, "/two-factor/generate-backup-codes", {
|
|
72
|
+
password: input.password
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
return failureOf(response, "No new backup codes were made");
|
|
36
76
|
}
|
|
77
|
+
const { backupCodes } = twoFactorSetupSchema.pick({ backupCodes: true }).parse(await response.json());
|
|
78
|
+
return { ok: true, backupCodes };
|
|
79
|
+
}
|
|
80
|
+
async function forgetTrustedBrowsers(apiBaseUrl) {
|
|
81
|
+
const response = await postAuth(apiBaseUrl, "/two-factor/forget-trusted-browsers", {});
|
|
82
|
+
return response.ok ? { ok: true } : failureOf(response, "The browsers are still trusted");
|
|
83
|
+
}
|
|
84
|
+
function postAuth(apiBaseUrl, path, body) {
|
|
85
|
+
return fetch(authUrl(apiBaseUrl, path), {
|
|
86
|
+
method: "POST",
|
|
87
|
+
credentials: "include",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify(body)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function failureOf(response, fallback) {
|
|
37
93
|
const failure = authFailureSchema.safeParse(await response.json().catch(() => void 0));
|
|
38
94
|
return {
|
|
39
95
|
ok: false,
|
|
40
|
-
|
|
96
|
+
status: response.status,
|
|
97
|
+
...failure.data?.code === void 0 ? {} : { code: failure.data.code },
|
|
98
|
+
message: failure.data?.message ?? failure.data?.error?.message ?? fallback
|
|
41
99
|
};
|
|
42
100
|
}
|
|
43
101
|
async function signInForSessionFetch(input) {
|
|
@@ -50,6 +108,12 @@ async function signInForSessionFetch(input) {
|
|
|
50
108
|
if (!response.ok) {
|
|
51
109
|
throw new Error(`Sign-in as '${input.email}' was refused with status ${response.status}`);
|
|
52
110
|
}
|
|
111
|
+
const answer = signInAnswerSchema.safeParse(
|
|
112
|
+
await response.clone().json().catch(() => void 0)
|
|
113
|
+
);
|
|
114
|
+
if (answer.data?.twoFactorRedirect === true) {
|
|
115
|
+
throw new Error(`'${input.email}' signs in with a second factor; use an API key for this tool`);
|
|
116
|
+
}
|
|
53
117
|
const cookie = response.headers.getSetCookie().map((value) => value.split(";", 1)[0]).join("; ");
|
|
54
118
|
return (resource, init) => {
|
|
55
119
|
const request = new Request(resource, init);
|
|
@@ -180,7 +244,9 @@ function readEventData(block) {
|
|
|
180
244
|
}
|
|
181
245
|
|
|
182
246
|
// src/transport.ts
|
|
183
|
-
import {
|
|
247
|
+
import {
|
|
248
|
+
isUnknownOperationResponse
|
|
249
|
+
} from "@work-shape/catalyst-api-contract";
|
|
184
250
|
function createApiTransport(options) {
|
|
185
251
|
const baseUrl = options.baseUrl.replace(/\/$/u, "");
|
|
186
252
|
const cookieMode = options.getToken === void 0;
|
|
@@ -209,10 +275,20 @@ function createApiTransport(options) {
|
|
|
209
275
|
if (operation.headers?.request?.includes("Idempotency-Key")) {
|
|
210
276
|
headers.set("idempotency-key", request.idempotencyKey ?? createIdempotencyKey());
|
|
211
277
|
}
|
|
278
|
+
if (request.unattended && operation.headers?.request?.includes("Catalyst-Unattended")) {
|
|
279
|
+
headers.set("catalyst-unattended", "1");
|
|
280
|
+
}
|
|
212
281
|
const token = await options.getToken?.();
|
|
282
|
+
const sessionSignal = options.signal;
|
|
283
|
+
const signal = sessionSignal && request.signal ? AbortSignal.any([sessionSignal, request.signal]) : sessionSignal ?? request.signal;
|
|
213
284
|
if (token) {
|
|
214
285
|
headers.set("authorization", `Bearer ${token}`);
|
|
215
286
|
}
|
|
287
|
+
const locale = options.locale;
|
|
288
|
+
if (locale !== void 0) {
|
|
289
|
+
headers.set("accept-language", locale);
|
|
290
|
+
}
|
|
291
|
+
signal?.throwIfAborted();
|
|
216
292
|
let response;
|
|
217
293
|
try {
|
|
218
294
|
response = await (options.fetchImpl ?? fetch)(url, {
|
|
@@ -220,10 +296,10 @@ function createApiTransport(options) {
|
|
|
220
296
|
headers,
|
|
221
297
|
body,
|
|
222
298
|
credentials: cookieMode ? "include" : "omit",
|
|
223
|
-
signal
|
|
299
|
+
signal
|
|
224
300
|
});
|
|
225
301
|
} catch (error) {
|
|
226
|
-
throw
|
|
302
|
+
throw signal?.aborted ? error : transportError(error);
|
|
227
303
|
}
|
|
228
304
|
if (response.ok) {
|
|
229
305
|
return response;
|
|
@@ -311,7 +387,13 @@ function hasEveryOperation(operations, tree) {
|
|
|
311
387
|
function callOperation(transport, operation, request) {
|
|
312
388
|
const { response } = operation;
|
|
313
389
|
if (response.kind === "sse") {
|
|
314
|
-
return streamEvents(transport, operation, response
|
|
390
|
+
return streamEvents(transport, operation, response, request);
|
|
391
|
+
}
|
|
392
|
+
if (response.kind === "redirect") {
|
|
393
|
+
return transport.send(operation, request).then(async (answer) => {
|
|
394
|
+
await answer.body?.cancel();
|
|
395
|
+
return void 0;
|
|
396
|
+
});
|
|
315
397
|
}
|
|
316
398
|
if (response.kind === "blob") {
|
|
317
399
|
return transport.send(operation, request).then((answer) => readBody(() => answer.blob(), request));
|
|
@@ -341,7 +423,7 @@ function callOperation(transport, operation, request) {
|
|
|
341
423
|
};
|
|
342
424
|
});
|
|
343
425
|
}
|
|
344
|
-
async function* streamEvents(transport, operation,
|
|
426
|
+
async function* streamEvents(transport, operation, response, request) {
|
|
345
427
|
const answer = await transport.send(operation, request);
|
|
346
428
|
if (answer.status === 204) {
|
|
347
429
|
request.onCaughtUp?.();
|
|
@@ -352,7 +434,12 @@ async function* streamEvents(transport, operation, schema, request) {
|
|
|
352
434
|
}
|
|
353
435
|
try {
|
|
354
436
|
for await (const data of readServerSentEvents(answer.body)) {
|
|
355
|
-
|
|
437
|
+
const event = readEvent(transport, data, response, answer.status);
|
|
438
|
+
if (event.skipped) {
|
|
439
|
+
request.onSkipped?.(event.value);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
yield event.value;
|
|
356
443
|
}
|
|
357
444
|
} catch (error) {
|
|
358
445
|
throw asCallFailure(error, request);
|
|
@@ -368,6 +455,22 @@ async function readBody(read, request) {
|
|
|
368
455
|
function asCallFailure(error, request) {
|
|
369
456
|
return error instanceof ApiError || request.signal?.aborted ? error : transportError(error);
|
|
370
457
|
}
|
|
458
|
+
function readEvent(transport, text, response, status) {
|
|
459
|
+
const { skip } = response;
|
|
460
|
+
if (skip) {
|
|
461
|
+
let value;
|
|
462
|
+
try {
|
|
463
|
+
value = JSON.parse(text);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw unreadableResponseError(status, error);
|
|
466
|
+
}
|
|
467
|
+
const parsed = response.schema.safeParse(value);
|
|
468
|
+
if (parsed.success) return { skipped: false, value: parsed.data };
|
|
469
|
+
const skipped = skip.safeParse(value);
|
|
470
|
+
if (skipped.success) return { skipped: true, value: skipped.data };
|
|
471
|
+
}
|
|
472
|
+
return { skipped: false, value: parseJson(transport, text, response.schema, status) };
|
|
473
|
+
}
|
|
371
474
|
function parseJson(transport, text, schema, status) {
|
|
372
475
|
let value;
|
|
373
476
|
try {
|
|
@@ -392,12 +495,17 @@ export {
|
|
|
392
495
|
completedOutput,
|
|
393
496
|
createApiClient,
|
|
394
497
|
createApiClientFor,
|
|
498
|
+
forgetTrustedBrowsers,
|
|
395
499
|
getAuthSession,
|
|
396
500
|
listAll,
|
|
397
501
|
refusalError,
|
|
502
|
+
regenerateBackupCodes,
|
|
398
503
|
signInForSessionFetch,
|
|
399
504
|
signInWithEmail,
|
|
400
505
|
signOut,
|
|
506
|
+
startTwoFactorSetup,
|
|
401
507
|
transportError,
|
|
402
|
-
|
|
508
|
+
turnOffTwoFactor,
|
|
509
|
+
unreadableResponseError,
|
|
510
|
+
verifySecondFactor
|
|
403
511
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@work-shape/catalyst-api-client",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
3
|
+
"version": "0.8.0-rc.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"zod": "4.4.3",
|
|
32
|
-
"@work-shape/catalyst-api-contract": "0.8.0-rc.
|
|
32
|
+
"@work-shape/catalyst-api-contract": "0.8.0-rc.11"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^24.13.1",
|