@cuvo-health-us/api 0.1.0-next.0
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 +264 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.js +50 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +59 -0
- package/dist/events.d.ts +126 -0
- package/dist/events.js +100 -0
- package/dist/generated/v1.d.ts +6371 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/pagination.d.ts +33 -0
- package/dist/pagination.js +46 -0
- package/dist/retry.d.ts +24 -0
- package/dist/retry.js +81 -0
- package/dist/webhooks.d.ts +43 -0
- package/dist/webhooks.js +78 -0
- package/package.json +48 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cuvo-health-us/api`: the TypeScript client for the Cuvo Integrations API.
|
|
3
|
+
*
|
|
4
|
+
* Every type in here is generated from `api/openapi/v1.yaml`, which is generated from the zod
|
|
5
|
+
* schemas the server validates with. Nothing about the contract is written by hand twice.
|
|
6
|
+
*/
|
|
7
|
+
export { type CuvoClient, type CuvoClientOptions, createCuvoClient, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, } from "./client.js";
|
|
8
|
+
export { CuvoApiError, type Problem, type ProblemIssue, unwrap } from "./errors.js";
|
|
9
|
+
export { type CuvoEvent, type CuvoEventOf, type CuvoEventResourceOf, type CuvoEventResourceType, type CuvoEventResourceTypeOf, type CuvoEventType, type CuvoTypedEvent, type ExpandedResource, expandEvent, typedEvent, } from "./events.js";
|
|
10
|
+
export type { components, operations, paths } from "./generated/v1.js";
|
|
11
|
+
export { type ListPage, paginate } from "./pagination.js";
|
|
12
|
+
export { DEFAULT_TOLERANCE_SECONDS, parseEvent, type VerifySignatureOptions, verifySignature, } from "./webhooks.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cuvo-health-us/api`: the TypeScript client for the Cuvo Integrations API.
|
|
3
|
+
*
|
|
4
|
+
* Every type in here is generated from `api/openapi/v1.yaml`, which is generated from the zod
|
|
5
|
+
* schemas the server validates with. Nothing about the contract is written by hand twice.
|
|
6
|
+
*/
|
|
7
|
+
export { createCuvoClient, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, } from "./client.js";
|
|
8
|
+
export { CuvoApiError, unwrap } from "./errors.js";
|
|
9
|
+
export { expandEvent, typedEvent, } from "./events.js";
|
|
10
|
+
export { paginate } from "./pagination.js";
|
|
11
|
+
export { DEFAULT_TOLERANCE_SECONDS, parseEvent, verifySignature, } from "./webhooks.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every list in the contract answers the same envelope and pages on the id of the last row.
|
|
3
|
+
* There is no total, so a caller who wants "all of them" writes the same loop every time. This
|
|
4
|
+
* is that loop.
|
|
5
|
+
*/
|
|
6
|
+
export interface ListPage<Item> {
|
|
7
|
+
data: Item[];
|
|
8
|
+
has_more: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Walk a list endpoint to its end, one item at a time.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const cases = paginate((starting_after) =>
|
|
15
|
+
* unwrap(
|
|
16
|
+
* await cuvo.GET("/v1/cases", {
|
|
17
|
+
* params: { query: { limit: 100, status: "in_review", starting_after } },
|
|
18
|
+
* }),
|
|
19
|
+
* ),
|
|
20
|
+
* );
|
|
21
|
+
* for await (const item of cases) console.log(item.id);
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The callback takes the cursor and returns a page, rather than this taking the filters itself,
|
|
25
|
+
* so the filters stay contextually typed by the endpoint: `status: "in_review"` is checked
|
|
26
|
+
* against that operation's enum instead of widening to `string`.
|
|
27
|
+
*
|
|
28
|
+
* The iterator is lazy. It fetches the next page only when the consumer asks for an item past
|
|
29
|
+
* the end of the current one, so `break` costs nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function paginate<Item extends {
|
|
32
|
+
id: string;
|
|
33
|
+
}>(listPage: (cursor: string | undefined) => Promise<ListPage<Item>>): AsyncGenerator<Item, void, undefined>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every list in the contract answers the same envelope and pages on the id of the last row.
|
|
3
|
+
* There is no total, so a caller who wants "all of them" writes the same loop every time. This
|
|
4
|
+
* is that loop.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Walk a list endpoint to its end, one item at a time.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const cases = paginate((starting_after) =>
|
|
11
|
+
* unwrap(
|
|
12
|
+
* await cuvo.GET("/v1/cases", {
|
|
13
|
+
* params: { query: { limit: 100, status: "in_review", starting_after } },
|
|
14
|
+
* }),
|
|
15
|
+
* ),
|
|
16
|
+
* );
|
|
17
|
+
* for await (const item of cases) console.log(item.id);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The callback takes the cursor and returns a page, rather than this taking the filters itself,
|
|
21
|
+
* so the filters stay contextually typed by the endpoint: `status: "in_review"` is checked
|
|
22
|
+
* against that operation's enum instead of widening to `string`.
|
|
23
|
+
*
|
|
24
|
+
* The iterator is lazy. It fetches the next page only when the consumer asks for an item past
|
|
25
|
+
* the end of the current one, so `break` costs nothing.
|
|
26
|
+
*/
|
|
27
|
+
export async function* paginate(listPage) {
|
|
28
|
+
let cursor;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const page = await listPage(cursor);
|
|
31
|
+
for (const item of page.data)
|
|
32
|
+
yield item;
|
|
33
|
+
const last = page.data.at(-1);
|
|
34
|
+
// An empty page that still claims there is more would loop forever on the same cursor. The
|
|
35
|
+
// server does not do that; a proxy answering from a stale cache might.
|
|
36
|
+
if (!page.has_more || last === undefined)
|
|
37
|
+
return;
|
|
38
|
+
// The other way to loop forever is a callback that ignores the cursor it was handed. That
|
|
39
|
+
// is a mistake in the caller's query, and it is worth saying so rather than paging silently
|
|
40
|
+
// over page one until the process dies.
|
|
41
|
+
if (last.id === cursor) {
|
|
42
|
+
throw new Error(`paginate() was handed the same page twice at ${cursor}. Pass the cursor through to the query as starting_after.`);
|
|
43
|
+
}
|
|
44
|
+
cursor = last.id;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retry ladder.
|
|
3
|
+
*
|
|
4
|
+
* It wraps `fetch` rather than sitting in openapi-fetch middleware, because middleware runs once
|
|
5
|
+
* per call and a retry has to re-issue the request the middleware already finished building:
|
|
6
|
+
* headers, idempotency key and all. Wrapping the transport means every attempt sends the exact
|
|
7
|
+
* bytes of the first one, which is the whole reason a replay is safe.
|
|
8
|
+
*/
|
|
9
|
+
export declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
10
|
+
export interface RetryingFetchOptions {
|
|
11
|
+
fetch: typeof globalThis.fetch;
|
|
12
|
+
/** Attempts after the first. `0` disables retrying. */
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
/** Injected by the tests; a ladder measured in real seconds is otherwise untestable. */
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
random?: () => number;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `Retry-After` is either a whole number of seconds or an HTTP date. Anything else is ignored
|
|
21
|
+
* rather than guessed at, and the caller falls back to the backoff ladder.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseRetryAfter(value: string | null, nowMs: number): number | undefined;
|
|
24
|
+
export declare function createRetryingFetch(options: RetryingFetchOptions): typeof globalThis.fetch;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retry ladder.
|
|
3
|
+
*
|
|
4
|
+
* It wraps `fetch` rather than sitting in openapi-fetch middleware, because middleware runs once
|
|
5
|
+
* per call and a retry has to re-issue the request the middleware already finished building:
|
|
6
|
+
* headers, idempotency key and all. Wrapping the transport means every attempt sends the exact
|
|
7
|
+
* bytes of the first one, which is the whole reason a replay is safe.
|
|
8
|
+
*/
|
|
9
|
+
/** Doubling from here, capped, so three retries cost under four seconds. */
|
|
10
|
+
const BASE_DELAY_MS = 500;
|
|
11
|
+
const MAX_DELAY_MS = 8_000;
|
|
12
|
+
/** A server may name any delay; we will not sit on a socket for longer than this. */
|
|
13
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
14
|
+
/** The writes that need a key before a replay is safe. GET and HEAD are always replayable. */
|
|
15
|
+
const WRITE_METHODS = new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
16
|
+
export const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
17
|
+
/**
|
|
18
|
+
* Retryable statuses: the rate limit, and the server faults that a second attempt can clear.
|
|
19
|
+
*
|
|
20
|
+
* 501 is excluded although it is a 5xx. The contract uses it for `live_not_available`, a
|
|
21
|
+
* permanent answer for the whole of Phase 1, so retrying it would add seconds of backoff to
|
|
22
|
+
* every live call that was never going to succeed.
|
|
23
|
+
*/
|
|
24
|
+
function isRetryable(status) {
|
|
25
|
+
return status === 429 || (status >= 500 && status !== 501);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `Retry-After` is either a whole number of seconds or an HTTP date. Anything else is ignored
|
|
29
|
+
* rather than guessed at, and the caller falls back to the backoff ladder.
|
|
30
|
+
*/
|
|
31
|
+
export function parseRetryAfter(value, nowMs) {
|
|
32
|
+
if (value === null)
|
|
33
|
+
return undefined;
|
|
34
|
+
const header = value.trim();
|
|
35
|
+
if (header.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
const seconds = /^\d+$/.test(header) ? Number(header) : Number.NaN;
|
|
38
|
+
const milliseconds = Number.isNaN(seconds) ? Date.parse(header) - nowMs : seconds * 1000;
|
|
39
|
+
if (Number.isNaN(milliseconds))
|
|
40
|
+
return undefined;
|
|
41
|
+
return Math.min(Math.max(milliseconds, 0), MAX_RETRY_AFTER_MS);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Equal jitter: half the ceiling, plus a random share of the other half. Full jitter can return
|
|
45
|
+
* a delay of nearly zero, which turns a fleet of clients retrying a 503 back into the stampede
|
|
46
|
+
* the backoff exists to break up.
|
|
47
|
+
*/
|
|
48
|
+
function backoffMs(attempt, random) {
|
|
49
|
+
const ceiling = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
50
|
+
return ceiling / 2 + random() * (ceiling / 2);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A write without an `Idempotency-Key` must never be retried: the first attempt may have been
|
|
54
|
+
* applied and answered into a socket that died, and a second one would create a second patient.
|
|
55
|
+
* The client mints a key for every write, so in practice this refuses only a request that
|
|
56
|
+
* reached the transport some other way.
|
|
57
|
+
*/
|
|
58
|
+
function mayRetry(request) {
|
|
59
|
+
if (!WRITE_METHODS.has(request.method.toUpperCase()))
|
|
60
|
+
return true;
|
|
61
|
+
return request.headers.has(IDEMPOTENCY_KEY_HEADER);
|
|
62
|
+
}
|
|
63
|
+
export function createRetryingFetch(options) {
|
|
64
|
+
const { fetch: transport, maxRetries, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), random = Math.random, now = Date.now, } = options;
|
|
65
|
+
return async (input, init) => {
|
|
66
|
+
// openapi-fetch always hands the transport a fully built Request; the other shapes are here
|
|
67
|
+
// so this stays a drop-in `fetch`.
|
|
68
|
+
const request = input instanceof Request && init === undefined ? input : new Request(input, init);
|
|
69
|
+
for (let attempt = 0;; attempt += 1) {
|
|
70
|
+
// A Request body can only be read once, so each attempt gets its own copy.
|
|
71
|
+
const response = await transport(request.clone());
|
|
72
|
+
if (attempt >= maxRetries || !isRetryable(response.status) || !mayRetry(request)) {
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
// Nothing will read this body, and an unread one holds the connection open.
|
|
76
|
+
await response.body?.cancel().catch(() => undefined);
|
|
77
|
+
const named = parseRetryAfter(response.headers.get("Retry-After"), now());
|
|
78
|
+
await sleep(named ?? backoffMs(attempt, random));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CuvoTypedEvent } from "./events.js";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook verification, the receiving half of the recipe in api/lib/webhooks/sign.ts:
|
|
4
|
+
*
|
|
5
|
+
* Cuvo-Signature: t=<unix seconds>,v1=<hex hmac_sha256(secret, `${t}.${rawBody}`)>
|
|
6
|
+
*
|
|
7
|
+
* Two properties this depends on. The digest covers `t`, so a captured delivery cannot be
|
|
8
|
+
* replayed under a fresh timestamp; and it covers the RAW body, so the bytes must be verified
|
|
9
|
+
* before any JSON parse. Read the body as text off the request and pass that string, never
|
|
10
|
+
* `JSON.stringify(req.body)`. A re-serialization changes key order and whitespace and every
|
|
11
|
+
* signature fails.
|
|
12
|
+
*
|
|
13
|
+
* During the 24 hour grace after a rotation the header carries TWO `v1=` values, the new secret
|
|
14
|
+
* first. Accepting when ANY of them matches is what lets a receiver move to the new secret
|
|
15
|
+
* whenever it likes inside that window.
|
|
16
|
+
*/
|
|
17
|
+
/** The contract's replay window, in seconds. */
|
|
18
|
+
export declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
19
|
+
export interface VerifySignatureOptions {
|
|
20
|
+
/** The delivery body as the exact bytes that arrived. */
|
|
21
|
+
rawBody: string;
|
|
22
|
+
/** The `Cuvo-Signature` header. A missing header is a failed verification, not a throw. */
|
|
23
|
+
signatureHeader: string | null | undefined;
|
|
24
|
+
/** The endpoint's signing secret, `whsec_…`. */
|
|
25
|
+
secret: string;
|
|
26
|
+
toleranceSeconds?: number;
|
|
27
|
+
/** Injected by the tests; a signature over a clock is otherwise untestable. */
|
|
28
|
+
now?: () => number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* True when the delivery was signed with this secret, recently. Reject the delivery with a 400
|
|
32
|
+
* when it is false, and never act on an unverified body.
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifySignature(options: VerifySignatureOptions): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Parse a verified delivery body into an event keyed by its `type`, so a handler branching on
|
|
37
|
+
* `event.type` gets the resource that type carries without a cast.
|
|
38
|
+
*
|
|
39
|
+
* Verify first. This does not check the signature, and it does not validate the payload against
|
|
40
|
+
* the contract; it checks only that the body is the shape an event has, so a proxy's error page
|
|
41
|
+
* fails here rather than three property accesses later.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseEvent(rawBody: string): CuvoTypedEvent;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { typedEvent } from "./events.js";
|
|
3
|
+
/**
|
|
4
|
+
* Webhook verification, the receiving half of the recipe in api/lib/webhooks/sign.ts:
|
|
5
|
+
*
|
|
6
|
+
* Cuvo-Signature: t=<unix seconds>,v1=<hex hmac_sha256(secret, `${t}.${rawBody}`)>
|
|
7
|
+
*
|
|
8
|
+
* Two properties this depends on. The digest covers `t`, so a captured delivery cannot be
|
|
9
|
+
* replayed under a fresh timestamp; and it covers the RAW body, so the bytes must be verified
|
|
10
|
+
* before any JSON parse. Read the body as text off the request and pass that string, never
|
|
11
|
+
* `JSON.stringify(req.body)`. A re-serialization changes key order and whitespace and every
|
|
12
|
+
* signature fails.
|
|
13
|
+
*
|
|
14
|
+
* During the 24 hour grace after a rotation the header carries TWO `v1=` values, the new secret
|
|
15
|
+
* first. Accepting when ANY of them matches is what lets a receiver move to the new secret
|
|
16
|
+
* whenever it likes inside that window.
|
|
17
|
+
*/
|
|
18
|
+
/** The contract's replay window, in seconds. */
|
|
19
|
+
export const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
20
|
+
/** Constant-time hex compare that tolerates a length mismatch instead of throwing on one. */
|
|
21
|
+
function matches(candidate, expected) {
|
|
22
|
+
const left = Buffer.from(candidate, "utf8");
|
|
23
|
+
const right = Buffer.from(expected, "utf8");
|
|
24
|
+
if (left.length !== right.length)
|
|
25
|
+
return false;
|
|
26
|
+
return timingSafeEqual(left, right);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when the delivery was signed with this secret, recently. Reject the delivery with a 400
|
|
30
|
+
* when it is false, and never act on an unverified body.
|
|
31
|
+
*/
|
|
32
|
+
export function verifySignature(options) {
|
|
33
|
+
const { rawBody, signatureHeader, secret, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now = Date.now, } = options;
|
|
34
|
+
if (!signatureHeader)
|
|
35
|
+
return false;
|
|
36
|
+
let timestamp;
|
|
37
|
+
const candidates = [];
|
|
38
|
+
for (const part of signatureHeader.split(",")) {
|
|
39
|
+
const separator = part.indexOf("=");
|
|
40
|
+
if (separator === -1)
|
|
41
|
+
continue;
|
|
42
|
+
const key = part.slice(0, separator).trim();
|
|
43
|
+
const value = part.slice(separator + 1).trim();
|
|
44
|
+
if (key === "t")
|
|
45
|
+
timestamp = value;
|
|
46
|
+
else if (key === "v1")
|
|
47
|
+
candidates.push(value);
|
|
48
|
+
}
|
|
49
|
+
if (timestamp === undefined || candidates.length === 0)
|
|
50
|
+
return false;
|
|
51
|
+
if (!/^\d+$/.test(timestamp))
|
|
52
|
+
return false;
|
|
53
|
+
const age = Math.abs(now() / 1000 - Number(timestamp));
|
|
54
|
+
if (age >= toleranceSeconds)
|
|
55
|
+
return false;
|
|
56
|
+
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
|
|
57
|
+
// Every candidate is compared; the set of signatures on the wire is not a secret, so there is
|
|
58
|
+
// nothing to leak by the number of comparisons, only by their duration.
|
|
59
|
+
return candidates.some((candidate) => matches(candidate, expected));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse a verified delivery body into an event keyed by its `type`, so a handler branching on
|
|
63
|
+
* `event.type` gets the resource that type carries without a cast.
|
|
64
|
+
*
|
|
65
|
+
* Verify first. This does not check the signature, and it does not validate the payload against
|
|
66
|
+
* the contract; it checks only that the body is the shape an event has, so a proxy's error page
|
|
67
|
+
* fails here rather than three property accesses later.
|
|
68
|
+
*/
|
|
69
|
+
export function parseEvent(rawBody) {
|
|
70
|
+
const body = JSON.parse(rawBody);
|
|
71
|
+
const event = body;
|
|
72
|
+
if (typeof event?.id !== "string" ||
|
|
73
|
+
typeof event.type !== "string" ||
|
|
74
|
+
typeof event.resource?.id !== "string") {
|
|
75
|
+
throw new Error("The body is not a Cuvo event.");
|
|
76
|
+
}
|
|
77
|
+
return typedEvent(event);
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cuvo-health-us/api",
|
|
3
|
+
"version": "0.1.0-next.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "TypeScript client for the Cuvo Integrations API, generated from the v1 contract.",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"openapi-fetch": "^0.17.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "2.4.16",
|
|
21
|
+
"@types/node": "^20",
|
|
22
|
+
"ajv": "^8.20.0",
|
|
23
|
+
"ajv-formats": "^3.0.1",
|
|
24
|
+
"knip": "^6.15.0",
|
|
25
|
+
"openapi-typescript": "^7.13.0",
|
|
26
|
+
"tsx": "^4.20.0",
|
|
27
|
+
"typescript": "^5",
|
|
28
|
+
"vitest": "^4.1.10",
|
|
29
|
+
"yaml": "^2.9.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"tag": "next"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"generate": "tsx scripts/generate-types.ts",
|
|
40
|
+
"check:generated": "tsx scripts/generate-types.ts --check",
|
|
41
|
+
"build": "tsc -p tsconfig.build.json && mkdir -p dist/generated && cp src/generated/v1.d.ts dist/generated/v1.d.ts",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "biome check .",
|
|
44
|
+
"format": "biome check --write .",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"gates": "pnpm typecheck && pnpm lint && pnpm exec knip && pnpm test && pnpm check:generated"
|
|
47
|
+
}
|
|
48
|
+
}
|