@work-shape/catalyst-api-client 0.8.0-rc.1 → 0.8.0-rc.10

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 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,6 +22,8 @@ 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;
26
28
  }
27
29
  declare function getAuthSession(apiBaseUrl: string): Promise<AuthSession | null>;
@@ -82,6 +84,13 @@ interface ApiClientOptions {
82
84
  */
83
85
  getToken?: () => string | undefined | Promise<string | undefined>;
84
86
  fetchImpl?: typeof fetch;
87
+ /** Cancels this client's requests when its owning session ends. */
88
+ signal?: AbortSignal;
89
+ /**
90
+ * The language every request asks the server to answer in, read at each request. A browser's
91
+ * own Accept-Language names its settings, not the language a person chose in the interface.
92
+ */
93
+ locale?: LocaleCode;
85
94
  /** Only available without a token source; browser navigation cannot send a bearer token. */
86
95
  browserManagedDownloads?: boolean;
87
96
  /**
@@ -127,7 +136,12 @@ type ControlPart<Op extends Operation> = {
127
136
  } ? {
128
137
  /** Called when the server has nothing after the given position and sends no stream. */
129
138
  onCaughtUp?: () => void;
130
- } : unknown);
139
+ } & (Op["response"] extends {
140
+ skip: infer Skip extends z.ZodType;
141
+ } ? {
142
+ /** Called for an event this client does not know, which the stream passes over. */
143
+ onSkipped?: (value: z.output<Skip>) => void;
144
+ } : unknown) : unknown);
131
145
  /**
132
146
  * An operation that takes an `Idempotency-Key` gets a new one with every call. A caller that
133
147
  * sends the same call again after an answer that never arrived passes the key of the first.
@@ -139,7 +153,18 @@ type IdempotencyPart<Op> = Op extends {
139
153
  } ? "Idempotency-Key" extends Header ? {
140
154
  idempotencyKey?: string;
141
155
  } : unknown : unknown;
142
- type InputOf<Op extends Operation> = ParamsPart<Op["path"]> & QueryPart<Op> & BodyPart<Op> & IdempotencyPart<Op> & ControlPart<Op>;
156
+ /**
157
+ * An operation that takes `Catalyst-Unattended` is sent with it when `unattended` is set: nobody
158
+ * watches the caller's terminal, so a call that needs a confirmation waits in the Inbox.
159
+ */
160
+ type UnattendedPart<Op> = Op extends {
161
+ headers: {
162
+ request: readonly (infer Header extends OperationRequestHeader)[];
163
+ };
164
+ } ? "Catalyst-Unattended" extends Header ? {
165
+ unattended?: boolean;
166
+ } : unknown : unknown;
167
+ type InputOf<Op extends Operation> = ParamsPart<Op["path"]> & QueryPart<Op> & BodyPart<Op> & IdempotencyPart<Op> & UnattendedPart<Op> & ControlPart<Op>;
143
168
  /**
144
169
  * How a call of an operation that may wait for an approval came out. A refusal and a failure
145
170
  * are thrown as `ApiError`, as for every operation.
@@ -153,7 +178,10 @@ type OperationOutcome<Output, Run = unknown> = {
153
178
  status: "done";
154
179
  output: Output;
155
180
  }
156
- /** Nothing ran yet: somebody else decides. `location` is where the run is read again. */
181
+ /**
182
+ * Nothing ran yet: somebody decides it, an approver or, for a run `pending_confirmation`, the
183
+ * caller in their Inbox. `location` is where the run is read again.
184
+ */
157
185
  | {
158
186
  status: "pending_approval";
159
187
  run: Run;
@@ -169,6 +197,8 @@ type OutputOf<Op extends Operation> = Op["response"] extends {
169
197
  kind: "sse";
170
198
  schema: infer Schema extends z.ZodType;
171
199
  } ? AsyncIterable<z.output<Schema>> : Op["response"] extends {
200
+ kind: "redirect";
201
+ } ? Promise<undefined> : Op["response"] extends {
172
202
  schema: infer Schema extends z.ZodType;
173
203
  } ? Op extends {
174
204
  accepted: infer Accepted extends z.ZodType;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ 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
  });
@@ -37,6 +38,7 @@ async function signInWithEmail(input) {
37
38
  const failure = authFailureSchema.safeParse(await response.json().catch(() => void 0));
38
39
  return {
39
40
  ok: false,
41
+ ...failure.data?.code === void 0 ? {} : { code: failure.data.code },
40
42
  message: failure.data?.message ?? failure.data?.error?.message ?? "Sign in failed"
41
43
  };
42
44
  }
@@ -180,7 +182,9 @@ function readEventData(block) {
180
182
  }
181
183
 
182
184
  // src/transport.ts
183
- import { isUnknownOperationResponse } from "@work-shape/catalyst-api-contract";
185
+ import {
186
+ isUnknownOperationResponse
187
+ } from "@work-shape/catalyst-api-contract";
184
188
  function createApiTransport(options) {
185
189
  const baseUrl = options.baseUrl.replace(/\/$/u, "");
186
190
  const cookieMode = options.getToken === void 0;
@@ -209,10 +213,20 @@ function createApiTransport(options) {
209
213
  if (operation.headers?.request?.includes("Idempotency-Key")) {
210
214
  headers.set("idempotency-key", request.idempotencyKey ?? createIdempotencyKey());
211
215
  }
216
+ if (request.unattended && operation.headers?.request?.includes("Catalyst-Unattended")) {
217
+ headers.set("catalyst-unattended", "1");
218
+ }
212
219
  const token = await options.getToken?.();
220
+ const sessionSignal = options.signal;
221
+ const signal = sessionSignal && request.signal ? AbortSignal.any([sessionSignal, request.signal]) : sessionSignal ?? request.signal;
213
222
  if (token) {
214
223
  headers.set("authorization", `Bearer ${token}`);
215
224
  }
225
+ const locale = options.locale;
226
+ if (locale !== void 0) {
227
+ headers.set("accept-language", locale);
228
+ }
229
+ signal?.throwIfAborted();
216
230
  let response;
217
231
  try {
218
232
  response = await (options.fetchImpl ?? fetch)(url, {
@@ -220,10 +234,10 @@ function createApiTransport(options) {
220
234
  headers,
221
235
  body,
222
236
  credentials: cookieMode ? "include" : "omit",
223
- signal: request.signal
237
+ signal
224
238
  });
225
239
  } catch (error) {
226
- throw request.signal?.aborted ? error : transportError(error);
240
+ throw signal?.aborted ? error : transportError(error);
227
241
  }
228
242
  if (response.ok) {
229
243
  return response;
@@ -311,7 +325,13 @@ function hasEveryOperation(operations, tree) {
311
325
  function callOperation(transport, operation, request) {
312
326
  const { response } = operation;
313
327
  if (response.kind === "sse") {
314
- return streamEvents(transport, operation, response.schema, request);
328
+ return streamEvents(transport, operation, response, request);
329
+ }
330
+ if (response.kind === "redirect") {
331
+ return transport.send(operation, request).then(async (answer) => {
332
+ await answer.body?.cancel();
333
+ return void 0;
334
+ });
315
335
  }
316
336
  if (response.kind === "blob") {
317
337
  return transport.send(operation, request).then((answer) => readBody(() => answer.blob(), request));
@@ -341,7 +361,7 @@ function callOperation(transport, operation, request) {
341
361
  };
342
362
  });
343
363
  }
344
- async function* streamEvents(transport, operation, schema, request) {
364
+ async function* streamEvents(transport, operation, response, request) {
345
365
  const answer = await transport.send(operation, request);
346
366
  if (answer.status === 204) {
347
367
  request.onCaughtUp?.();
@@ -352,7 +372,12 @@ async function* streamEvents(transport, operation, schema, request) {
352
372
  }
353
373
  try {
354
374
  for await (const data of readServerSentEvents(answer.body)) {
355
- yield parseJson(transport, data, schema, answer.status);
375
+ const event = readEvent(transport, data, response, answer.status);
376
+ if (event.skipped) {
377
+ request.onSkipped?.(event.value);
378
+ continue;
379
+ }
380
+ yield event.value;
356
381
  }
357
382
  } catch (error) {
358
383
  throw asCallFailure(error, request);
@@ -368,6 +393,22 @@ async function readBody(read, request) {
368
393
  function asCallFailure(error, request) {
369
394
  return error instanceof ApiError || request.signal?.aborted ? error : transportError(error);
370
395
  }
396
+ function readEvent(transport, text, response, status) {
397
+ const { skip } = response;
398
+ if (skip) {
399
+ let value;
400
+ try {
401
+ value = JSON.parse(text);
402
+ } catch (error) {
403
+ throw unreadableResponseError(status, error);
404
+ }
405
+ const parsed = response.schema.safeParse(value);
406
+ if (parsed.success) return { skipped: false, value: parsed.data };
407
+ const skipped = skip.safeParse(value);
408
+ if (skipped.success) return { skipped: true, value: skipped.data };
409
+ }
410
+ return { skipped: false, value: parseJson(transport, text, response.schema, status) };
411
+ }
371
412
  function parseJson(transport, text, schema, status) {
372
413
  let value;
373
414
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@work-shape/catalyst-api-client",
3
- "version": "0.8.0-rc.1",
3
+ "version": "0.8.0-rc.10",
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.1"
32
+ "@work-shape/catalyst-api-contract": "0.8.0-rc.10"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^24.13.1",