diff --git a/.github/copilot-instructions.md b/AGENTS.md similarity index 73% rename from .github/copilot-instructions.md rename to AGENTS.md index 00039aa..2400152 100644 --- a/.github/copilot-instructions.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Copilot Instructions for `counterfact/apis` +# Repository Instructions for `counterfact/apis` This repository contains Counterfact-based API simulators. @@ -21,18 +21,27 @@ Generate and evolve simulator code **one API (or coherent API subset) at a time* - response status - response body/headers as applicable - resulting state changes - placeholder `dummy` test files. - -3. **Implement state and business logic in route context files** + - no placeholder `dummy` test files + +3. **Use middleware for shared cross-cutting request behavior** + - Put authentication, authorization, and other behavior shared by all routes + in a scope into a `routes/**/_.middleware.ts` file. + - Export a `middleware` function that either returns a response or calls + `respondTo($)` to continue the request chain. + - Keep operation-specific behavior in route handlers; do not duplicate a + uniform authentication check in every handler. + - Follow Counterfact's [middleware pattern](https://github.com/counterfact/api-simulator/blob/main/docs/features/middleware.md), including its path-scoping and chaining semantics. + +4. **Implement state and business logic in route context files** - Put simulator state and business rules in `routes/**/_.context.ts`. - Do not edit generated `types/_.context.ts` files. - Keep route handlers thin by delegating behavior to context classes/methods. -4. **Unit test Context classes directly** +5. **Unit test Context classes directly** - Add direct unit tests for `Context` class behavior in `routes/**/_.context.ts`. - Cover state transitions and core business logic independently of HTTP tests. -5. **Use scenarios for startup init and REPL setup flows** +6. **Use scenarios for startup init and REPL setup flows** - Use `startup` to initialize simulator state when the server starts. - Use other scenario functions for REPL-invoked setup/actions after startup. - Keep scenarios simple and declarative. diff --git a/README.md b/README.md index 74e7554..0b65f58 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ This repository hosts API simulation packages built with [`counterfact`](https:/ - [`@counterfact/swagger-pet-store`](./swagger-pet-store): simulator package generated from the Swagger Petstore OpenAPI spec. - [`@counterfact/github`](./github): simulator package generated from GitHub's OpenAPI spec. +- [`@counterfact/ordergroove`](./ordergroove): combined simulator package generated from Ordergroove's Customers, Items, Offers, Orders, Products, and Subscriptions OpenAPI specs. diff --git a/ordergroove/.gitignore b/ordergroove/.gitignore new file mode 100644 index 0000000..9c24728 --- /dev/null +++ b/ordergroove/.gitignore @@ -0,0 +1,2 @@ +.cache +node_modules diff --git a/ordergroove/.prettierignore b/ordergroove/.prettierignore new file mode 100644 index 0000000..9ed96bf --- /dev/null +++ b/ordergroove/.prettierignore @@ -0,0 +1,2 @@ +.cache +openapi diff --git a/ordergroove/README.md b/ordergroove/README.md new file mode 100644 index 0000000..1f9d1e6 --- /dev/null +++ b/ordergroove/README.md @@ -0,0 +1,148 @@ +# Ordergroove simulator + +This package runs the Customers, Items, Offers, Orders, Products, and +Subscriptions REST APIs together on one Counterfact server. It uses +Counterfact `2.14.2` and serves every operation at its canonical Ordergroove +path, without an API-group prefix. + +## Install and start + +From this directory, install the locked dependencies and start the HTTP server: + +```sh +cd ordergroove +npm ci +npm run serve +``` + +The server listens at `http://localhost:3100` by default. Use `npm start` instead +when you want Counterfact's full interactive development mode, including its +REPL and file watching. Stop either process with Ctrl-C. Restarting the server +resets all resources to the deterministic startup data below. + +## Authentication + +All REST operations require the `x-api-key` request header. The simulator's +local test key is: + +```text +ordergroove-local-api-key +``` + +Missing or incorrect keys receive a `401 Unauthorized` response. For example: + +```sh +curl \ + -H 'x-api-key: ordergroove-local-api-key' \ + http://localhost:3100/customers/ +``` + +## Canonical API URLs + +All six specifications are mounted on the same origin: + +| API | Canonical collection URLs | +| ------------- | --------------------------------------------- | +| Customers | `/customers/` | +| Products | `/products/` | +| Offers | `/offer_profiles/`, `/otd/`, `/entitlements/` | +| Subscriptions | `/subscriptions/` | +| Orders | `/orders/` | +| Items | `/items/` | + +Detail and action URLs extend those paths directly. Examples include +`/customers/customer-001/`, `/products/product-001/`, +`/subscriptions/subscription-001/cancel/`, `/orders/order-001/send_now/`, and +`/items/item-001/`. Paths such as `/customers/customers/` or +`/subscriptions/subscriptions/` do not exist. + +## Seeded data + +Every API has a `startup` scenario. Together they create two coherent commerce +chains: + +| Customer | Product | Offer profile | Subscription | Order | Item | +| ----------------------------- | ----------------------------- | ------------------- | ------------------ | ----------- | ---------- | +| `customer-001` (Ada Lovelace) | `product-001` (`sku-coffee`) | `offer-profile-001` | `subscription-001` | `order-001` | `item-001` | +| `customer-002` (Grace Hopper) | `product-002` (`sku-filters`) | `offer-profile-002` | `subscription-002` | `order-002` | `item-002` | + +The first subscription is live and monthly; the second is inactive and runs +every two weeks. `order-001` starts unsent, while `order-002` starts successful. +The offer data also includes `discount-001` for `customer-001` and three +entitlements: two for `customer-001` and one for `customer-002`. + +State changes persist for the lifetime of the server. Created customers, +one-time discounts, and items can be retrieved or listed afterward; product, +customer, subscription, and order updates are also visible to later requests. + +## Example flows + +Inspect the seeded resources for the first customer: + +```sh +curl -H 'x-api-key: ordergroove-local-api-key' \ + 'http://localhost:3100/subscriptions/?customer=customer-001' + +curl -H 'x-api-key: ordergroove-local-api-key' \ + 'http://localhost:3100/orders/?customer=customer-001' + +curl -H 'x-api-key: ordergroove-local-api-key' \ + 'http://localhost:3100/items/?subscription=subscription-001' + +curl -H 'x-api-key: ordergroove-local-api-key' \ + 'http://localhost:3100/entitlements/?customer=customer-001' +``` + +Cancel and reactivate a subscription: + +```sh +curl -X POST -H 'x-api-key: ordergroove-local-api-key' \ + http://localhost:3100/subscriptions/subscription-001/cancel/ + +curl -X POST -H 'x-api-key: ordergroove-local-api-key' \ + http://localhost:3100/subscriptions/subscription-001/reactivate/ +``` + +Send the seeded unsent order immediately, then retrieve its persisted pending +state: + +```sh +curl -X POST -H 'x-api-key: ordergroove-local-api-key' \ + http://localhost:3100/orders/order-001/send_now/ + +curl -H 'x-api-key: ordergroove-local-api-key' \ + http://localhost:3100/orders/order-001/ +``` + +Create a one-time item linked to the first seeded chain: + +```sh +curl -X POST \ + -H 'x-api-key: ordergroove-local-api-key' \ + -H 'content-type: application/json' \ + --data '{"order_id":"order-001","subscription_id":"subscription-001","product_id":"product-001","quantity":1,"price":"19.99","total_price":"19.99","offer_id":"offer-profile-001","one_time":true}' \ + http://localhost:3100/items/ +``` + +Unknown detail IDs return realistic `404` responses. Collection filters include +customer and status for orders; subscription and order for items; customer, +product, live state, and documented creation dates for subscriptions; and +customer for entitlements. + +## Contracts and scope + +`openapi/upstream/` contains the six unchanged published REST contracts. The +multi-spec `counterfact.yaml` consumes them directly with an empty prefix for +each API; there are no normalized contract copies or duplicated group paths. + +Ordergroove's Early Access GraphQL API is explicitly out of scope because no +public schema or confirmed endpoint is available. + +## Validation + +Run the complete package checks from `ordergroove/`: + +```sh +npm test +npm run lint +``` diff --git a/ordergroove/counterfact.yaml b/ordergroove/counterfact.yaml new file mode 100644 index 0000000..5caeb1f --- /dev/null +++ b/ordergroove/counterfact.yaml @@ -0,0 +1,20 @@ +spec: + - source: ./openapi/upstream/customers.yml + group: customers + prefix: "" + - source: ./openapi/upstream/items.yml + group: items + prefix: "" + - source: ./openapi/upstream/offers.yml + group: offers + prefix: "" + - source: ./openapi/upstream/orders.yml + group: orders + prefix: "" + - source: ./openapi/upstream/products.yml + group: products + prefix: "" + - source: ./openapi/upstream/subscriptions.yml + group: subscriptions + prefix: "" +destination: . diff --git a/ordergroove/customers/.gitignore b/ordergroove/customers/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/customers/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/customers/counterfact-types/cookie-options.ts b/ordergroove/customers/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/customers/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/customers/counterfact-types/counterfact-response.ts b/ordergroove/customers/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/customers/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/customers/counterfact-types/example-names.ts b/ordergroove/customers/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/customers/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/customers/counterfact-types/example.ts b/ordergroove/customers/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/customers/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/customers/counterfact-types/generic-response-builder.ts b/ordergroove/customers/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/customers/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/customers/counterfact-types/http-status-code.ts b/ordergroove/customers/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/customers/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/customers/counterfact-types/if-has-key.ts b/ordergroove/customers/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/customers/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/customers/counterfact-types/index.ts b/ordergroove/customers/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/customers/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/customers/counterfact-types/maybe-promise.ts b/ordergroove/customers/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/customers/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/customers/counterfact-types/media-type.ts b/ordergroove/customers/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/customers/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/customers/counterfact-types/omit-all.ts b/ordergroove/customers/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/customers/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/customers/counterfact-types/omit-value-when-never.ts b/ordergroove/customers/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/customers/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/customers/counterfact-types/open-api-content.ts b/ordergroove/customers/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/customers/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/customers/counterfact-types/open-api-header.ts b/ordergroove/customers/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/customers/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/customers/counterfact-types/open-api-operation.ts b/ordergroove/customers/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/customers/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/customers/counterfact-types/open-api-parameters.ts b/ordergroove/customers/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/customers/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/customers/counterfact-types/open-api-response.ts b/ordergroove/customers/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/customers/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/customers/counterfact-types/random-function.ts b/ordergroove/customers/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/customers/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/customers/counterfact-types/response-builder-factory.ts b/ordergroove/customers/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/customers/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/customers/counterfact-types/response-builder.ts b/ordergroove/customers/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/customers/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/customers/counterfact-types/wide-operation-argument.ts b/ordergroove/customers/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/customers/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/customers/counterfact-types/wide-response-builder.ts b/ordergroove/customers/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/customers/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/customers/routes/_.context.ts b/ordergroove/customers/routes/_.context.ts new file mode 100644 index 0000000..a4df68b --- /dev/null +++ b/ordergroove/customers/routes/_.context.ts @@ -0,0 +1,97 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Customer } from "../types/components/schemas/Customer.js"; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #customers = new Map(); + #nextCustomerNumber = 1; + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedCustomers(customers: Customer[]): void { + this.#customers.clear(); + for (const customer of customers) { + if (customer.public_id) { + this.#customers.set(customer.public_id, structuredClone(customer)); + } + } + this.#nextCustomerNumber = this.#findNextCustomerNumber(); + } + + listCustomers(): Customer[] { + return [...this.#customers.values()].map((customer) => + structuredClone(customer), + ); + } + + getCustomer(publicId: string): Customer | undefined { + const customer = this.#customers.get(publicId); + return customer ? structuredClone(customer) : undefined; + } + + createCustomer(input: Customer): Customer { + const customerNumber = this.#nextAvailableCustomerNumber(); + const suffix = String(customerNumber).padStart(3, "0"); + const publicId = input.public_id ?? `customer-${suffix}`; + const customer = { + ...structuredClone(input), + id: input.id ?? `customer-internal-${suffix}`, + public_id: publicId, + }; + + this.#customers.set(publicId, customer); + return structuredClone(customer); + } + + replaceCustomer(publicId: string, input: Customer): Customer | undefined { + const existing = this.#customers.get(publicId); + if (!existing) return undefined; + + const customer = { + ...structuredClone(input), + id: existing.id, + public_id: publicId, + }; + this.#customers.set(publicId, customer); + return structuredClone(customer); + } + + #findNextCustomerNumber(): number { + let next = 1; + while (this.#customers.has(`customer-${String(next).padStart(3, "0")}`)) { + next += 1; + } + return next; + } + + #nextAvailableCustomerNumber(): number { + const current = this.#nextCustomerNumber; + this.#nextCustomerNumber += 1; + while ( + this.#customers.has( + `customer-${String(this.#nextCustomerNumber).padStart(3, "0")}`, + ) + ) { + this.#nextCustomerNumber += 1; + } + return current; + } +} diff --git a/ordergroove/customers/routes/_.middleware.ts b/ordergroove/customers/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/customers/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/customers/routes/customers.ts b/ordergroove/customers/routes/customers.ts new file mode 100644 index 0000000..f3abedd --- /dev/null +++ b/ordergroove/customers/routes/customers.ts @@ -0,0 +1,14 @@ +import type { listCustomers } from "../types/paths/customers.types.js"; +import type { createCustomer } from "../types/paths/customers.types.js"; + +export const GET: listCustomers = async ($) => { + return $.response[200].json({ + results: $.context.listCustomers(), + next: null, + previous: null, + } as never); +}; + +export const POST: createCustomer = async ($) => { + return $.response[201].json($.context.createCustomer($.body)); +}; diff --git a/ordergroove/customers/routes/customers/{public_id}.ts b/ordergroove/customers/routes/customers/{public_id}.ts new file mode 100644 index 0000000..171ff2d --- /dev/null +++ b/ordergroove/customers/routes/customers/{public_id}.ts @@ -0,0 +1,16 @@ +import type { retrieveCustomer } from "../../types/paths/customers/{public_id}.types.js"; +import type { updateCustomer } from "../../types/paths/customers/{public_id}.types.js"; + +export const GET: retrieveCustomer = async ($) => { + const customer = $.context.getCustomer($.path.public_id); + return customer + ? $.response[200].json(customer) + : $.x.response[404].json({ error: "Customer not found" }); +}; + +export const PUT: updateCustomer = async ($) => { + const customer = $.context.replaceCustomer($.path.public_id, $.body); + return customer + ? $.response[200].json(customer) + : $.x.response[404].json({ error: "Customer not found" }); +}; diff --git a/ordergroove/customers/scenarios/index.ts b/ordergroove/customers/scenarios/index.ts new file mode 100644 index 0000000..37543fc --- /dev/null +++ b/ordergroove/customers/scenarios/index.ts @@ -0,0 +1,75 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedCustomers([ + { + id: "customer-internal-001", + public_id: "customer-001", + merchant_id: "merchant-001", + merchant_user_id: "user-001", + first_name: "Ada", + last_name: "Lovelace", + email: "ada@example.com", + }, + { + id: "customer-internal-002", + public_id: "customer-002", + merchant_id: "merchant-001", + merchant_user_id: "user-002", + first_name: "Grace", + last_name: "Hopper", + email: "grace@example.com", + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/customers/test/context.test.ts b/ordergroove/customers/test/context.test.ts new file mode 100644 index 0000000..87551f1 --- /dev/null +++ b/ordergroove/customers/test/context.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds, lists, and retrieves customers without exposing mutable state", () => { + const context = createContext(); + context.seedCustomers([ + { + id: "customer-internal-001", + public_id: "customer-001", + merchant_id: "merchant-001", + merchant_user_id: "user-001", + first_name: "Ada", + last_name: "Lovelace", + email: "ada@example.com", + }, + ]); + + const listed = context.listCustomers(); + assert.equal(listed.length, 1); + assert.equal(context.getCustomer("customer-001")?.email, "ada@example.com"); + + listed[0]!.email = "changed@example.com"; + assert.equal(context.getCustomer("customer-001")?.email, "ada@example.com"); +}); + +test("creates customers with deterministic identifiers and persists them", () => { + const context = createContext(); + context.seedCustomers([]); + + const first = context.createCustomer({ + merchant_user_id: "user-new", + email: "new@example.com", + }); + const second = context.createCustomer({ email: "second@example.com" }); + + assert.equal(first.id, "customer-internal-001"); + assert.equal(first.public_id, "customer-001"); + assert.equal(second.id, "customer-internal-002"); + assert.equal(second.public_id, "customer-002"); + assert.deepEqual(context.getCustomer("customer-001"), first); +}); + +test("replacement updates preserve identifiers and remove omitted fields", () => { + const context = createContext(); + context.seedCustomers([ + { + id: "customer-internal-007", + public_id: "customer-007", + first_name: "Old", + last_name: "Name", + email: "old@example.com", + }, + ]); + + const updated = context.replaceCustomer("customer-007", { + first_name: "New", + email: "new@example.com", + id: "ignored-id", + public_id: "ignored-public-id", + }); + + assert.deepEqual(updated, { + id: "customer-internal-007", + public_id: "customer-007", + first_name: "New", + email: "new@example.com", + }); + assert.equal( + context.replaceCustomer("missing", { first_name: "Nobody" }), + undefined, + ); +}); diff --git a/ordergroove/customers/test/routes.test.ts b/ordergroove/customers/test/routes.test.ts new file mode 100644 index 0000000..dc647b5 --- /dev/null +++ b/ordergroove/customers/test/routes.test.ts @@ -0,0 +1,210 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; +import type { Context } from "../routes/_.context.ts"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/customers.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; +let context: Context; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/customers/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + context = app.contextRegistry.find("/") as Context; + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key", async () => { + const missing = await fetch(`http://127.0.0.1:${port}/customers/`); + assert.equal(missing.status, 401); + assert.deepEqual(await missing.json(), { error: "Unauthorized" }); + + const invalid = await request("/customers/", { + headers: { "x-api-key": "invalid" }, + }); + assert.equal(invalid.status, 401); +}); + +test("serves every API at its canonical collection path", async () => { + for (const pathname of [ + "/customers/", + "/items/", + "/offer_profiles/", + "/orders/", + "/products/", + "/subscriptions/", + ]) { + const response = await request(pathname); + assert.notEqual(response.status, 404, `${pathname} should be registered`); + } + + const duplicated = await request("/customers/customers"); + assert.equal(duplicated.status, 404); +}); + +test("lists deterministic startup customers", async () => { + const response = await request("/customers/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + results: [ + { + id: "customer-internal-001", + public_id: "customer-001", + merchant_id: "merchant-001", + merchant_user_id: "user-001", + first_name: "Ada", + last_name: "Lovelace", + email: "ada@example.com", + }, + { + id: "customer-internal-002", + public_id: "customer-002", + merchant_id: "merchant-001", + merchant_user_id: "user-002", + first_name: "Grace", + last_name: "Hopper", + email: "grace@example.com", + }, + ], + next: null, + previous: null, + }); +}); + +test("creates and retrieves a persisted customer", async () => { + const createResponse = await request("/customers/", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + merchant_id: "merchant-001", + merchant_user_id: "user-003", + first_name: "Katherine", + last_name: "Johnson", + email: "katherine@example.com", + }), + }); + assert.equal(createResponse.status, 201); + const created = await createResponse.json(); + assert.equal(created.id, "customer-internal-003"); + assert.equal(created.public_id, "customer-003"); + assert.equal( + context.getCustomer("customer-003")?.email, + "katherine@example.com", + ); + + const retrieveResponse = await request("/customers/customer-003/"); + assert.equal(retrieveResponse.status, 200); + assert.deepEqual(await retrieveResponse.json(), created); +}); + +test("replaces a customer and returns 404 for unknown customers", async () => { + const updateResponse = await request("/customers/customer-003/", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + first_name: "Katherine", + last_name: "Gobble", + email: "kg@example.com", + }), + }); + assert.equal(updateResponse.status, 200); + assert.deepEqual(await updateResponse.json(), { + id: "customer-internal-003", + public_id: "customer-003", + first_name: "Katherine", + last_name: "Gobble", + email: "kg@example.com", + }); + assert.equal(context.getCustomer("customer-003")?.merchant_id, undefined); + + const missingGet = await request("/customers/not-found/"); + assert.equal(missingGet.status, 404); + assert.deepEqual(await missingGet.json(), { error: "Customer not found" }); + + const missingPut = await request("/customers/not-found/", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ first_name: "Nobody" }), + }); + assert.equal(missingPut.status, 404); +}); diff --git a/ordergroove/customers/types/#/components/responses/Unauthorized.ts b/ordergroove/customers/types/#/components/responses/Unauthorized.ts new file mode 100644 index 0000000..d6753c8 --- /dev/null +++ b/ordergroove/customers/types/#/components/responses/Unauthorized.ts @@ -0,0 +1,6 @@ +export type Unauthorized = { + headers: never; + requiredHeaders: never; + content: never; + examples: {}; +}; diff --git a/ordergroove/customers/types/_.context.ts b/ordergroove/customers/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/customers/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/customers/types/components/schemas/Customer.ts b/ordergroove/customers/types/components/schemas/Customer.ts new file mode 100644 index 0000000..fe4c7c2 --- /dev/null +++ b/ordergroove/customers/types/components/schemas/Customer.ts @@ -0,0 +1,9 @@ +export type Customer = { + id?: string; + public_id?: string; + merchant_id?: string; + merchant_user_id?: string; + first_name?: string; + last_name?: string; + email?: string; +}; diff --git a/ordergroove/customers/types/components/schemas/CustomerList.ts b/ordergroove/customers/types/components/schemas/CustomerList.ts new file mode 100644 index 0000000..af569a7 --- /dev/null +++ b/ordergroove/customers/types/components/schemas/CustomerList.ts @@ -0,0 +1,7 @@ +import type { Customer } from "./Customer.js"; + +export type CustomerList = { + results?: Array; + next?: string; + previous?: string; +}; diff --git a/ordergroove/customers/types/paths/customers.types.ts b/ordergroove/customers/types/paths/customers.types.ts new file mode 100644 index 0000000..b2bc0f3 --- /dev/null +++ b/ordergroove/customers/types/paths/customers.types.ts @@ -0,0 +1,84 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { CustomerList } from "../components/schemas/CustomerList.js"; +import type { Unauthorized } from "../#/components/responses/Unauthorized.js"; +import type { Customer } from "../components/schemas/Customer.js"; + +/** + * List customers + */ +export type listCustomers = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: listCustomers_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: CustomerList; + }; + }; + examples: {}; + }; + 401: Unauthorized; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Create a customer + */ +export type createCustomer = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: createCustomer_Headers; + cookie: never; + body: Customer; + context: Context; + response: ResponseBuilderFactory<{ + 201: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Customer; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listCustomers_Headers = { "x-api-key": string }; + +export type createCustomer_Headers = { "x-api-key": string }; diff --git a/ordergroove/customers/types/paths/customers/{public_id}.types.ts b/ordergroove/customers/types/paths/customers/{public_id}.types.ts new file mode 100644 index 0000000..e3fc459 --- /dev/null +++ b/ordergroove/customers/types/paths/customers/{public_id}.types.ts @@ -0,0 +1,85 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts"; +import type { Context } from "../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts"; +import type { Customer } from "../../components/schemas/Customer.js"; + +/** + * Retrieve a customer + */ +export type retrieveCustomer = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: retrieveCustomer_Path; + headers: retrieveCustomer_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Customer; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Update a customer + */ +export type updateCustomer = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: updateCustomer_Path; + headers: updateCustomer_Headers; + cookie: never; + body: Customer; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Customer; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type retrieveCustomer_Path = { public_id: string }; + +export type retrieveCustomer_Headers = { "x-api-key": string }; + +export type updateCustomer_Path = { public_id: string }; + +export type updateCustomer_Headers = { "x-api-key": string }; diff --git a/ordergroove/eslint.config.mjs b/ordergroove/eslint.config.mjs new file mode 100644 index 0000000..640ad60 --- /dev/null +++ b/ordergroove/eslint.config.mjs @@ -0,0 +1,34 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +const typedFiles = ["*/scenarios/**/*.ts"]; + +export default tseslint.config( + { + ignores: ["*/.cache/**"], + }, + js.configs.recommended, + { + languageOptions: { + globals: { + fetch: "readonly", + module: "readonly", + process: "readonly", + require: "readonly", + setTimeout: "readonly", + URLSearchParams: "readonly", + }, + parserOptions: { + projectService: true, + }, + }, + }, + ...tseslint.configs.strictTypeChecked.map((config) => ({ + ...config, + files: typedFiles, + })), + ...tseslint.configs.stylisticTypeChecked.map((config) => ({ + ...config, + files: typedFiles, + })), +); diff --git a/ordergroove/items/.gitignore b/ordergroove/items/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/items/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/items/counterfact-types/cookie-options.ts b/ordergroove/items/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/items/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/items/counterfact-types/counterfact-response.ts b/ordergroove/items/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/items/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/items/counterfact-types/example-names.ts b/ordergroove/items/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/items/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/items/counterfact-types/example.ts b/ordergroove/items/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/items/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/items/counterfact-types/generic-response-builder.ts b/ordergroove/items/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/items/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/items/counterfact-types/http-status-code.ts b/ordergroove/items/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/items/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/items/counterfact-types/if-has-key.ts b/ordergroove/items/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/items/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/items/counterfact-types/index.ts b/ordergroove/items/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/items/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/items/counterfact-types/maybe-promise.ts b/ordergroove/items/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/items/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/items/counterfact-types/media-type.ts b/ordergroove/items/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/items/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/items/counterfact-types/omit-all.ts b/ordergroove/items/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/items/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/items/counterfact-types/omit-value-when-never.ts b/ordergroove/items/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/items/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/items/counterfact-types/open-api-content.ts b/ordergroove/items/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/items/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/items/counterfact-types/open-api-header.ts b/ordergroove/items/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/items/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/items/counterfact-types/open-api-operation.ts b/ordergroove/items/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/items/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/items/counterfact-types/open-api-parameters.ts b/ordergroove/items/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/items/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/items/counterfact-types/open-api-response.ts b/ordergroove/items/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/items/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/items/counterfact-types/random-function.ts b/ordergroove/items/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/items/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/items/counterfact-types/response-builder-factory.ts b/ordergroove/items/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/items/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/items/counterfact-types/response-builder.ts b/ordergroove/items/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/items/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/items/counterfact-types/wide-operation-argument.ts b/ordergroove/items/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/items/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/items/counterfact-types/wide-response-builder.ts b/ordergroove/items/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/items/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/items/routes/_.context.ts b/ordergroove/items/routes/_.context.ts new file mode 100644 index 0000000..2ab9568 --- /dev/null +++ b/ordergroove/items/routes/_.context.ts @@ -0,0 +1,98 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Item } from "../types/components/schemas/Item.js"; + +export type ItemFilters = { + subscription?: string; + order?: string; +}; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #items = new Map(); + #nextItemNumber = 1; + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedItems(items: Item[]): void { + this.#items.clear(); + + for (const item of items) { + if (item.public_id) { + this.#items.set(item.public_id, structuredClone(item)); + } + } + + this.#nextItemNumber = this.#findNextItemNumber(); + } + + listItems(filters: ItemFilters): Item[] { + return [...this.#items.values()] + .filter( + (item) => + (!filters.subscription || + item.subscription_id === filters.subscription) && + (!filters.order || item.order_id === filters.order), + ) + .map((item) => structuredClone(item)); + } + + getItem(publicId: string): Item | undefined { + const item = this.#items.get(publicId); + return item ? structuredClone(item) : undefined; + } + + createItem(input: Item): Item { + const itemNumber = this.#nextAvailableItemNumber(); + const suffix = String(itemNumber).padStart(3, "0"); + const publicId = input.public_id ?? `item-${suffix}`; + const item = { + ...structuredClone(input), + id: input.id ?? `item-internal-${suffix}`, + public_id: publicId, + }; + + this.#items.set(publicId, item); + return structuredClone(item); + } + + deleteItem(publicId: string): boolean { + return this.#items.delete(publicId); + } + + #findNextItemNumber(): number { + let next = 1; + while (this.#items.has(`item-${String(next).padStart(3, "0")}`)) { + next += 1; + } + return next; + } + + #nextAvailableItemNumber(): number { + const current = this.#nextItemNumber; + this.#nextItemNumber += 1; + while ( + this.#items.has(`item-${String(this.#nextItemNumber).padStart(3, "0")}`) + ) { + this.#nextItemNumber += 1; + } + return current; + } +} diff --git a/ordergroove/items/routes/_.middleware.ts b/ordergroove/items/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/items/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/items/routes/items.ts b/ordergroove/items/routes/items.ts new file mode 100644 index 0000000..97912c9 --- /dev/null +++ b/ordergroove/items/routes/items.ts @@ -0,0 +1,14 @@ +import type { listItems } from "../types/paths/items.types.js"; +import type { createItem } from "../types/paths/items.types.js"; + +export const GET: listItems = async ($) => { + return $.response[200].json({ + results: $.context.listItems($.query), + next: null, + previous: null, + } as never); +}; + +export const POST: createItem = async ($) => { + return $.response[201].json($.context.createItem($.body)); +}; diff --git a/ordergroove/items/routes/items/{public_id}.ts b/ordergroove/items/routes/items/{public_id}.ts new file mode 100644 index 0000000..9c10ffb --- /dev/null +++ b/ordergroove/items/routes/items/{public_id}.ts @@ -0,0 +1,15 @@ +import type { retrieveItem } from "../../types/paths/items/{public_id}.types.js"; +import type { deleteItem } from "../../types/paths/items/{public_id}.types.js"; + +export const GET: retrieveItem = async ($) => { + const item = $.context.getItem($.path.public_id); + return item + ? $.response[200].json(item) + : $.x.response[404].json({ error: "Item not found" }); +}; + +export const DELETE: deleteItem = async ($) => { + return $.context.deleteItem($.path.public_id) + ? $.response[204].empty() + : $.x.response[404].json({ error: "Item not found" }); +}; diff --git a/ordergroove/items/scenarios/index.ts b/ordergroove/items/scenarios/index.ts new file mode 100644 index 0000000..851135f --- /dev/null +++ b/ordergroove/items/scenarios/index.ts @@ -0,0 +1,81 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedItems([ + { + id: "item-internal-001", + public_id: "item-001", + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-001", + quantity: 1, + price: "19.99", + total_price: "19.99", + offer_id: "offer-profile-001", + one_time: false, + }, + { + id: "item-internal-002", + public_id: "item-002", + order_id: "order-002", + subscription_id: "subscription-002", + product_id: "product-002", + quantity: 2, + price: "12.50", + total_price: "25.00", + offer_id: "offer-profile-002", + one_time: false, + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/items/test/context.test.ts b/ordergroove/items/test/context.test.ts new file mode 100644 index 0000000..5711563 --- /dev/null +++ b/ordergroove/items/test/context.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +const seed = (context: Context) => { + context.seedItems([ + { + id: "item-internal-001", + public_id: "item-001", + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-001", + quantity: 1, + price: "19.99", + total_price: "19.99", + offer_id: "offer-profile-001", + one_time: false, + }, + { + id: "item-internal-002", + public_id: "item-002", + order_id: "order-002", + subscription_id: "subscription-002", + product_id: "product-002", + quantity: 2, + price: "12.50", + total_price: "25.00", + offer_id: "offer-profile-002", + one_time: false, + }, + ]); +}; + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds, lists, and retrieves items without exposing mutable state", () => { + const context = createContext(); + seed(context); + + const listed = context.listItems({}); + assert.equal(listed.length, 2); + assert.equal(context.getItem("item-001")?.product_id, "product-001"); + + listed[0]!.quantity = 99; + assert.equal(context.getItem("item-001")?.quantity, 1); +}); + +test("filters items by subscription and order", () => { + const context = createContext(); + seed(context); + + assert.deepEqual( + context + .listItems({ subscription: "subscription-001" }) + .map(({ public_id }) => public_id), + ["item-001"], + ); + assert.deepEqual( + context.listItems({ order: "order-002" }).map(({ public_id }) => public_id), + ["item-002"], + ); + assert.deepEqual( + context.listItems({ + subscription: "subscription-001", + order: "order-002", + }), + [], + ); +}); + +test("creates items with deterministic identifiers and persists them", () => { + const context = createContext(); + seed(context); + + const created = context.createItem({ + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-002", + quantity: 3, + price: "12.50", + total_price: "37.50", + one_time: true, + }); + + assert.equal(created.id, "item-internal-003"); + assert.equal(created.public_id, "item-003"); + assert.deepEqual(context.getItem("item-003"), created); +}); + +test("deletes items persistently and reports whether an item existed", () => { + const context = createContext(); + seed(context); + + assert.equal(context.deleteItem("item-001"), true); + assert.equal(context.getItem("item-001"), undefined); + assert.equal(context.deleteItem("item-001"), false); +}); diff --git a/ordergroove/items/test/routes.test.ts b/ordergroove/items/test/routes.test.ts new file mode 100644 index 0000000..5e83092 --- /dev/null +++ b/ordergroove/items/test/routes.test.ts @@ -0,0 +1,204 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/items.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/items/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key for collection and detail operations", async () => { + for (const [pathname, method, body] of [ + ["/items/", "GET", undefined], + ["/items/", "POST", JSON.stringify({ product_id: "product-001" })], + ["/items/item-001/", "GET", undefined], + ["/items/item-001/", "DELETE", undefined], + ] as const) { + const response = await fetch(`http://127.0.0.1:${port}${pathname}`, { + method, + body, + headers: body ? { "content-type": "application/json" } : undefined, + }); + assert.equal(response.status, 401, `${method} ${pathname}`); + assert.deepEqual(await response.json(), { error: "Unauthorized" }); + } +}); + +test("lists coherent deterministic items and filters by subscription and order", async () => { + const response = await request("/items/"); + assert.equal(response.status, 200); + const body = await response.json(); + assert.deepEqual( + body.results.map(({ public_id }: { public_id: string }) => public_id), + ["item-001", "item-002"], + ); + assert.equal(body.results[0].subscription_id, "subscription-001"); + assert.equal(body.results[0].order_id, "order-001"); + assert.equal(body.results[0].product_id, "product-001"); + + for (const [query, expected] of [ + ["subscription=subscription-001", ["item-001"]], + ["order=order-002", ["item-002"]], + ["subscription=subscription-001&order=order-002", []], + ] as const) { + const filtered = await request(`/items/?${query}`); + assert.equal(filtered.status, 200); + assert.deepEqual( + (await filtered.json()).results.map( + ({ public_id }: { public_id: string }) => public_id, + ), + expected, + query, + ); + } +}); + +test("retrieves an item", async () => { + const response = await request("/items/item-001/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + id: "item-internal-001", + public_id: "item-001", + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-001", + quantity: 1, + price: "19.99", + total_price: "19.99", + offer_id: "offer-profile-001", + one_time: false, + }); +}); + +test("creates an item and persists it", async () => { + const input = { + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-002", + quantity: 3, + price: "12.50", + total_price: "37.50", + offer_id: "offer-profile-001", + one_time: true, + }; + const response = await request("/items/", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(input), + }); + assert.equal(response.status, 201); + const created = await response.json(); + assert.deepEqual(created, { + ...input, + id: "item-internal-003", + public_id: "item-003", + }); + + const persisted = await request("/items/item-003/"); + assert.equal(persisted.status, 200); + assert.deepEqual(await persisted.json(), created); +}); + +test("deletes an item with an empty 204 response and persists deletion", async () => { + const response = await request("/items/item-002/", { method: "DELETE" }); + assert.equal(response.status, 204); + assert.equal(await response.text(), ""); + + const persisted = await request("/items/item-002/"); + assert.equal(persisted.status, 404); + assert.deepEqual(await persisted.json(), { error: "Item not found" }); +}); + +test("returns 404 for retrieve and delete on unknown items", async () => { + const retrieve = await request("/items/not-found/"); + assert.equal(retrieve.status, 404); + assert.deepEqual(await retrieve.json(), { error: "Item not found" }); + + const deletion = await request("/items/not-found/", { method: "DELETE" }); + assert.equal(deletion.status, 404); + assert.deepEqual(await deletion.json(), { error: "Item not found" }); +}); diff --git a/ordergroove/items/types/_.context.ts b/ordergroove/items/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/items/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/items/types/components/schemas/Item.ts b/ordergroove/items/types/components/schemas/Item.ts new file mode 100644 index 0000000..7b1c73b --- /dev/null +++ b/ordergroove/items/types/components/schemas/Item.ts @@ -0,0 +1,12 @@ +export type Item = { + id?: string; + public_id?: string; + order_id?: string; + subscription_id?: string; + product_id?: string; + quantity?: number; + price?: string; + total_price?: string; + offer_id?: string; + one_time?: boolean; +}; diff --git a/ordergroove/items/types/components/schemas/ItemList.ts b/ordergroove/items/types/components/schemas/ItemList.ts new file mode 100644 index 0000000..0c35ac9 --- /dev/null +++ b/ordergroove/items/types/components/schemas/ItemList.ts @@ -0,0 +1,7 @@ +import type { Item } from "./Item.js"; + +export type ItemList = { + results?: Array; + next?: string; + previous?: string; +}; diff --git a/ordergroove/items/types/paths/items.types.ts b/ordergroove/items/types/paths/items.types.ts new file mode 100644 index 0000000..b58278c --- /dev/null +++ b/ordergroove/items/types/paths/items.types.ts @@ -0,0 +1,84 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { ItemList } from "../components/schemas/ItemList.js"; +import type { Item } from "../components/schemas/Item.js"; + +/** + * List items + */ +export type listItems = ( + $: OmitValueWhenNever<{ + query: listItems_Query; + querystring: never; + path: never; + headers: listItems_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: ItemList; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Create an item + */ +export type createItem = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: createItem_Headers; + cookie: never; + body: Item; + context: Context; + response: ResponseBuilderFactory<{ + 201: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Item; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listItems_Query = { subscription?: string; order?: string }; + +export type listItems_Headers = { "x-api-key": string }; + +export type createItem_Headers = { "x-api-key": string }; diff --git a/ordergroove/items/types/paths/items/{public_id}.types.ts b/ordergroove/items/types/paths/items/{public_id}.types.ts new file mode 100644 index 0000000..2aa6899 --- /dev/null +++ b/ordergroove/items/types/paths/items/{public_id}.types.ts @@ -0,0 +1,81 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts"; +import type { Context } from "../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts"; +import type { Item } from "../../components/schemas/Item.js"; + +/** + * Retrieve an item + */ +export type retrieveItem = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: retrieveItem_Path; + headers: retrieveItem_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Item; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Delete an item + */ +export type deleteItem = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: deleteItem_Path; + headers: deleteItem_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 204: { + headers: never; + requiredHeaders: never; + content: never; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type retrieveItem_Path = { public_id: string }; + +export type retrieveItem_Headers = { "x-api-key": string }; + +export type deleteItem_Path = { public_id: string }; + +export type deleteItem_Headers = { "x-api-key": string }; diff --git a/ordergroove/offers/.gitignore b/ordergroove/offers/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/offers/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/offers/counterfact-types/cookie-options.ts b/ordergroove/offers/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/offers/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/offers/counterfact-types/counterfact-response.ts b/ordergroove/offers/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/offers/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/offers/counterfact-types/example-names.ts b/ordergroove/offers/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/offers/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/offers/counterfact-types/example.ts b/ordergroove/offers/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/offers/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/offers/counterfact-types/generic-response-builder.ts b/ordergroove/offers/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/offers/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/offers/counterfact-types/http-status-code.ts b/ordergroove/offers/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/offers/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/offers/counterfact-types/if-has-key.ts b/ordergroove/offers/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/offers/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/offers/counterfact-types/index.ts b/ordergroove/offers/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/offers/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/offers/counterfact-types/maybe-promise.ts b/ordergroove/offers/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/offers/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/offers/counterfact-types/media-type.ts b/ordergroove/offers/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/offers/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/offers/counterfact-types/omit-all.ts b/ordergroove/offers/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/offers/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/offers/counterfact-types/omit-value-when-never.ts b/ordergroove/offers/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/offers/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/offers/counterfact-types/open-api-content.ts b/ordergroove/offers/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/offers/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/offers/counterfact-types/open-api-header.ts b/ordergroove/offers/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/offers/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/offers/counterfact-types/open-api-operation.ts b/ordergroove/offers/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/offers/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/offers/counterfact-types/open-api-parameters.ts b/ordergroove/offers/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/offers/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/offers/counterfact-types/open-api-response.ts b/ordergroove/offers/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/offers/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/offers/counterfact-types/random-function.ts b/ordergroove/offers/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/offers/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/offers/counterfact-types/response-builder-factory.ts b/ordergroove/offers/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/offers/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/offers/counterfact-types/response-builder.ts b/ordergroove/offers/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/offers/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/offers/counterfact-types/wide-operation-argument.ts b/ordergroove/offers/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/offers/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/offers/counterfact-types/wide-response-builder.ts b/ordergroove/offers/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/offers/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/offers/routes/_.context.ts b/ordergroove/offers/routes/_.context.ts new file mode 100644 index 0000000..ceeab33 --- /dev/null +++ b/ordergroove/offers/routes/_.context.ts @@ -0,0 +1,111 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Entitlement } from "../types/components/schemas/Entitlement.js"; +import type { OfferProfile } from "../types/components/schemas/OfferProfile.js"; +import type { OneTimeDiscount } from "../types/components/schemas/OneTimeDiscount.js"; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #offerProfiles = new Map(); + readonly #oneTimeDiscounts = new Map(); + readonly #entitlements = new Map(); + #nextDiscountNumber = 1; + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedOfferProfiles(offerProfiles: OfferProfile[]): void { + this.#offerProfiles.clear(); + for (const offerProfile of offerProfiles) { + if (offerProfile.id) { + this.#offerProfiles.set(offerProfile.id, structuredClone(offerProfile)); + } + } + } + + listOfferProfiles(): OfferProfile[] { + return [...this.#offerProfiles.values()].map((offerProfile) => + structuredClone(offerProfile), + ); + } + + seedOneTimeDiscounts(discounts: OneTimeDiscount[]): void { + this.#oneTimeDiscounts.clear(); + for (const discount of discounts) { + if (discount.id) { + this.#oneTimeDiscounts.set(discount.id, structuredClone(discount)); + } + } + this.#nextDiscountNumber = this.#findNextDiscountNumber(); + } + + listOneTimeDiscounts(): OneTimeDiscount[] { + return [...this.#oneTimeDiscounts.values()].map((discount) => + structuredClone(discount), + ); + } + + createOneTimeDiscount(input: OneTimeDiscount): OneTimeDiscount { + const id = input.id ?? this.#nextDiscountId(); + const discount = { ...structuredClone(input), id }; + this.#oneTimeDiscounts.set(id, discount); + return structuredClone(discount); + } + + seedEntitlements(entitlements: Entitlement[]): void { + this.#entitlements.clear(); + for (const entitlement of entitlements) { + if (entitlement.id) { + this.#entitlements.set(entitlement.id, structuredClone(entitlement)); + } + } + } + + listEntitlements(customerId?: string): Entitlement[] { + return [...this.#entitlements.values()] + .filter( + (entitlement) => + customerId === undefined || entitlement.customer_id === customerId, + ) + .map((entitlement) => structuredClone(entitlement)); + } + + #findNextDiscountNumber(): number { + let next = 1; + while ( + this.#oneTimeDiscounts.has(`discount-${String(next).padStart(3, "0")}`) + ) { + next += 1; + } + return next; + } + + #nextDiscountId(): string { + const id = `discount-${String(this.#nextDiscountNumber).padStart(3, "0")}`; + this.#nextDiscountNumber += 1; + while ( + this.#oneTimeDiscounts.has( + `discount-${String(this.#nextDiscountNumber).padStart(3, "0")}`, + ) + ) { + this.#nextDiscountNumber += 1; + } + return id; + } +} diff --git a/ordergroove/offers/routes/_.middleware.ts b/ordergroove/offers/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/offers/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/offers/routes/entitlements.ts b/ordergroove/offers/routes/entitlements.ts new file mode 100644 index 0000000..bee1d3e --- /dev/null +++ b/ordergroove/offers/routes/entitlements.ts @@ -0,0 +1,7 @@ +import type { listEntitlements } from "../types/paths/entitlements.types.js"; + +export const GET: listEntitlements = async ($) => { + return $.response[200].json({ + results: $.context.listEntitlements($.query.customer), + }); +}; diff --git a/ordergroove/offers/routes/offer_profiles.ts b/ordergroove/offers/routes/offer_profiles.ts new file mode 100644 index 0000000..b714196 --- /dev/null +++ b/ordergroove/offers/routes/offer_profiles.ts @@ -0,0 +1,5 @@ +import type { listOfferProfiles } from "../types/paths/offer_profiles.types.js"; + +export const GET: listOfferProfiles = async ($) => { + return $.response[200].json({ results: $.context.listOfferProfiles() }); +}; diff --git a/ordergroove/offers/routes/otd.ts b/ordergroove/offers/routes/otd.ts new file mode 100644 index 0000000..ef8dbf6 --- /dev/null +++ b/ordergroove/offers/routes/otd.ts @@ -0,0 +1,12 @@ +import type { listOneTimeDiscounts } from "../types/paths/otd.types.js"; +import type { createOneTimeDiscount } from "../types/paths/otd.types.js"; + +export const GET: listOneTimeDiscounts = async ($) => { + return $.response[200].json({ + results: $.context.listOneTimeDiscounts(), + }); +}; + +export const POST: createOneTimeDiscount = async ($) => { + return $.response[201].json($.context.createOneTimeDiscount($.body)); +}; diff --git a/ordergroove/offers/scenarios/index.ts b/ordergroove/offers/scenarios/index.ts new file mode 100644 index 0000000..e8eb705 --- /dev/null +++ b/ordergroove/offers/scenarios/index.ts @@ -0,0 +1,92 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedOfferProfiles([ + { + id: "offer-profile-001", + name: "Subscribe and save", + description: "Save 10% on recurring deliveries", + }, + { + id: "offer-profile-002", + name: "VIP subscriber", + description: "Preferred pricing for VIP subscribers", + }, + ]); + context.seedOneTimeDiscounts([ + { + id: "discount-001", + customer_id: "customer-001", + amount: "5.00", + type: "fixed", + }, + ]); + context.seedEntitlements([ + { + id: "entitlement-001", + customer_id: "customer-001", + status: "active", + }, + { + id: "entitlement-002", + customer_id: "customer-002", + status: "active", + }, + { + id: "entitlement-003", + customer_id: "customer-001", + status: "expired", + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/offers/test/context.test.ts b/ordergroove/offers/test/context.test.ts new file mode 100644 index 0000000..f51a142 --- /dev/null +++ b/ordergroove/offers/test/context.test.ts @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds and lists offer profiles without exposing mutable state", () => { + const context = createContext(); + context.seedOfferProfiles([ + { + id: "offer-profile-001", + name: "Subscribe and save", + description: "Save 10% on recurring deliveries", + }, + ]); + + const profiles = context.listOfferProfiles(); + profiles[0]!.name = "Changed"; + + assert.equal(context.listOfferProfiles()[0]?.name, "Subscribe and save"); +}); + +test("creates deterministic one-time discounts and persists them", () => { + const context = createContext(); + context.seedOneTimeDiscounts([ + { + id: "discount-001", + customer_id: "customer-001", + amount: "5.00", + type: "fixed", + }, + ]); + + const created = context.createOneTimeDiscount({ + customer_id: "customer-002", + amount: "15.00", + type: "fixed", + }); + + assert.deepEqual(created, { + id: "discount-002", + customer_id: "customer-002", + amount: "15.00", + type: "fixed", + }); + assert.deepEqual(context.listOneTimeDiscounts(), [ + { + id: "discount-001", + customer_id: "customer-001", + amount: "5.00", + type: "fixed", + }, + created, + ]); +}); + +test("lists all entitlements or filters them by customer", () => { + const context = createContext(); + context.seedEntitlements([ + { + id: "entitlement-001", + customer_id: "customer-001", + status: "active", + }, + { + id: "entitlement-002", + customer_id: "customer-002", + status: "expired", + }, + ]); + + assert.equal(context.listEntitlements().length, 2); + assert.deepEqual(context.listEntitlements("customer-002"), [ + { + id: "entitlement-002", + customer_id: "customer-002", + status: "expired", + }, + ]); + assert.deepEqual(context.listEntitlements("unknown"), []); +}); diff --git a/ordergroove/offers/test/routes.test.ts b/ordergroove/offers/test/routes.test.ts new file mode 100644 index 0000000..2d68dfb --- /dev/null +++ b/ordergroove/offers/test/routes.test.ts @@ -0,0 +1,188 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/offers.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/offer_profiles/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key", async () => { + const missing = await fetch(`http://127.0.0.1:${port}/offer_profiles/`); + assert.equal(missing.status, 401); + assert.deepEqual(await missing.json(), { error: "Unauthorized" }); + + const invalid = await request("/entitlements/", { + headers: { "x-api-key": "invalid" }, + }); + assert.equal(invalid.status, 401); +}); + +test("lists deterministic startup offer profiles", async () => { + const response = await request("/offer_profiles/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + results: [ + { + id: "offer-profile-001", + name: "Subscribe and save", + description: "Save 10% on recurring deliveries", + }, + { + id: "offer-profile-002", + name: "VIP subscriber", + description: "Preferred pricing for VIP subscribers", + }, + ], + }); +}); + +test("lists and persists created one-time discounts", async () => { + const initial = await request("/otd/"); + assert.equal(initial.status, 200); + assert.deepEqual(await initial.json(), { + results: [ + { + id: "discount-001", + customer_id: "customer-001", + amount: "5.00", + type: "fixed", + }, + ], + }); + + const createResponse = await request("/otd/", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + customer_id: "customer-002", + amount: "15.00", + type: "fixed", + }), + }); + assert.equal(createResponse.status, 201); + const created = await createResponse.json(); + assert.deepEqual(created, { + id: "discount-002", + customer_id: "customer-002", + amount: "15.00", + type: "fixed", + }); + + const persisted = await request("/otd/"); + assert.equal(persisted.status, 200); + assert.equal((await persisted.json()).results.length, 2); +}); + +test("lists entitlements and filters by customer", async () => { + const allResponse = await request("/entitlements/"); + assert.equal(allResponse.status, 200); + assert.equal((await allResponse.json()).results.length, 3); + + const filteredResponse = await request( + "/entitlements/?customer=customer-001", + ); + assert.equal(filteredResponse.status, 200); + assert.deepEqual(await filteredResponse.json(), { + results: [ + { + id: "entitlement-001", + customer_id: "customer-001", + status: "active", + }, + { + id: "entitlement-003", + customer_id: "customer-001", + status: "expired", + }, + ], + }); + + const emptyResponse = await request("/entitlements/?customer=unknown"); + assert.equal(emptyResponse.status, 200); + assert.deepEqual(await emptyResponse.json(), { results: [] }); +}); diff --git a/ordergroove/offers/types/_.context.ts b/ordergroove/offers/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/offers/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/offers/types/components/schemas/Entitlement.ts b/ordergroove/offers/types/components/schemas/Entitlement.ts new file mode 100644 index 0000000..9ce652e --- /dev/null +++ b/ordergroove/offers/types/components/schemas/Entitlement.ts @@ -0,0 +1,5 @@ +export type Entitlement = { + id?: string; + customer_id?: string; + status?: string; +}; diff --git a/ordergroove/offers/types/components/schemas/OfferProfile.ts b/ordergroove/offers/types/components/schemas/OfferProfile.ts new file mode 100644 index 0000000..8ab521b --- /dev/null +++ b/ordergroove/offers/types/components/schemas/OfferProfile.ts @@ -0,0 +1 @@ +export type OfferProfile = { id?: string; name?: string; description?: string }; diff --git a/ordergroove/offers/types/components/schemas/OneTimeDiscount.ts b/ordergroove/offers/types/components/schemas/OneTimeDiscount.ts new file mode 100644 index 0000000..7f27d34 --- /dev/null +++ b/ordergroove/offers/types/components/schemas/OneTimeDiscount.ts @@ -0,0 +1,6 @@ +export type OneTimeDiscount = { + id?: string; + customer_id?: string; + amount?: string; + type?: string; +}; diff --git a/ordergroove/offers/types/paths/entitlements.types.ts b/ordergroove/offers/types/paths/entitlements.types.ts new file mode 100644 index 0000000..a2a2128 --- /dev/null +++ b/ordergroove/offers/types/paths/entitlements.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { Entitlement } from "../components/schemas/Entitlement.js"; + +/** + * List entitlements + */ +export type listEntitlements = ( + $: OmitValueWhenNever<{ + query: listEntitlements_Query; + querystring: never; + path: never; + headers: listEntitlements_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: { results?: Array }; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listEntitlements_Query = { customer?: string }; + +export type listEntitlements_Headers = { "x-api-key": string }; diff --git a/ordergroove/offers/types/paths/offer_profiles.types.ts b/ordergroove/offers/types/paths/offer_profiles.types.ts new file mode 100644 index 0000000..a095c60 --- /dev/null +++ b/ordergroove/offers/types/paths/offer_profiles.types.ts @@ -0,0 +1,46 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { OfferProfile } from "../components/schemas/OfferProfile.js"; + +/** + * Lists the offer profiles (incentive logic) that can be applied to recurring purchases. + */ +export type listOfferProfiles = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: listOfferProfiles_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: { results?: Array }; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listOfferProfiles_Headers = { "x-api-key": string }; diff --git a/ordergroove/offers/types/paths/otd.types.ts b/ordergroove/offers/types/paths/otd.types.ts new file mode 100644 index 0000000..f9ae100 --- /dev/null +++ b/ordergroove/offers/types/paths/otd.types.ts @@ -0,0 +1,81 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { OneTimeDiscount } from "../components/schemas/OneTimeDiscount.js"; + +/** + * List one-time discounts + */ +export type listOneTimeDiscounts = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: listOneTimeDiscounts_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: { results?: Array }; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Create a one-time discount + */ +export type createOneTimeDiscount = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: createOneTimeDiscount_Headers; + cookie: never; + body: OneTimeDiscount; + context: Context; + response: ResponseBuilderFactory<{ + 201: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: OneTimeDiscount; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listOneTimeDiscounts_Headers = { "x-api-key": string }; + +export type createOneTimeDiscount_Headers = { "x-api-key": string }; diff --git a/ordergroove/openapi/upstream/customers.yml b/ordergroove/openapi/upstream/customers.yml new file mode 100644 index 0000000..5a437d6 --- /dev/null +++ b/ordergroove/openapi/upstream/customers.yml @@ -0,0 +1,136 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Customers + description: Central customer profiles. +paths: + /customers/: + get: + operationId: listCustomers + tags: + - Customers + summary: List customers + responses: + '200': + description: A paginated list of customers. + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerList' + '401': + $ref: '#/components/responses/Unauthorized' + post: + operationId: createCustomer + tags: + - Customers + summary: Create a customer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + responses: + '201': + description: The created customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + /customers/{public_id}/: + get: + operationId: retrieveCustomer + tags: + - Customers + summary: Retrieve a customer + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: A customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + put: + operationId: updateCustomer + tags: + - Customers + summary: Update a customer + parameters: + - $ref: '#/components/parameters/PublicId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + responses: + '200': + description: The updated customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' +components: + responses: + Unauthorized: + description: Authentication failed or the API key is missing or invalid. + parameters: + PublicId: + name: public_id + in: path + required: true + schema: + type: string + description: The public identifier of the resource. + schemas: + Customer: + type: object + properties: + id: + type: string + public_id: + type: string + merchant_id: + type: string + merchant_user_id: + type: string + first_name: + type: string + last_name: + type: string + email: + type: string + CustomerList: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Customer' + next: + type: string + nullable: true + previous: + type: string + nullable: true + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/openapi/upstream/items.yml b/ordergroove/openapi/upstream/items.yml new file mode 100644 index 0000000..4930258 --- /dev/null +++ b/ordergroove/openapi/upstream/items.yml @@ -0,0 +1,137 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers Items API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Items + description: Line items within orders and subscriptions. +paths: + /items/: + get: + operationId: listItems + tags: + - Items + summary: List items + parameters: + - name: subscription + in: query + schema: + type: string + - name: order + in: query + schema: + type: string + responses: + '200': + description: A paginated list of items. + content: + application/json: + schema: + $ref: '#/components/schemas/ItemList' + post: + operationId: createItem + tags: + - Items + summary: Create an item + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '201': + description: The created item. + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + /items/{public_id}/: + get: + operationId: retrieveItem + tags: + - Items + summary: Retrieve an item + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: An item. + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + delete: + operationId: deleteItem + tags: + - Items + summary: Delete an item + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '204': + description: The item was deleted. +components: + parameters: + PublicId: + name: public_id + in: path + required: true + schema: + type: string + description: The public identifier of the resource. + schemas: + ItemList: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Item' + next: + type: string + nullable: true + previous: + type: string + nullable: true + Item: + type: object + properties: + id: + type: string + public_id: + type: string + order_id: + type: string + subscription_id: + type: string + nullable: true + product_id: + type: string + quantity: + type: integer + price: + type: string + total_price: + type: string + offer_id: + type: string + one_time: + type: boolean + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/openapi/upstream/offers.yml b/ordergroove/openapi/upstream/offers.yml new file mode 100644 index 0000000..3733a30 --- /dev/null +++ b/ordergroove/openapi/upstream/offers.yml @@ -0,0 +1,134 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers Offers API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Offers + description: Offers, incentives, discounts, and entitlements. +paths: + /offer_profiles/: + get: + operationId: listOfferProfiles + tags: + - Offers + summary: List offer profiles + description: Lists the offer profiles (incentive logic) that can be applied to recurring purchases. + responses: + '200': + description: A list of offer profiles. + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/OfferProfile' + /otd/: + get: + operationId: listOneTimeDiscounts + tags: + - Offers + summary: List one-time discounts + responses: + '200': + description: A list of one-time discounts. + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/OneTimeDiscount' + post: + operationId: createOneTimeDiscount + tags: + - Offers + summary: Create a one-time discount + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OneTimeDiscount' + responses: + '201': + description: The created one-time discount. + content: + application/json: + schema: + $ref: '#/components/schemas/OneTimeDiscount' + /entitlements/: + get: + operationId: listEntitlements + tags: + - Offers + summary: List entitlements + parameters: + - name: customer + in: query + schema: + type: string + responses: + '200': + description: A list of entitlements. + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Entitlement' +components: + schemas: + OneTimeDiscount: + type: object + properties: + id: + type: string + customer_id: + type: string + amount: + type: string + type: + type: string + Entitlement: + type: object + properties: + id: + type: string + customer_id: + type: string + status: + type: string + OfferProfile: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/openapi/upstream/orders.yml b/ordergroove/openapi/upstream/orders.yml new file mode 100644 index 0000000..3ba53cf --- /dev/null +++ b/ordergroove/openapi/upstream/orders.yml @@ -0,0 +1,147 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers Orders API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Orders + description: Recurring orders generated by subscriptions. +paths: + /orders/: + get: + operationId: listOrders + tags: + - Orders + summary: List orders + parameters: + - name: customer + in: query + schema: + type: string + - name: status + in: query + schema: + type: string + responses: + '200': + description: A paginated list of orders. + content: + application/json: + schema: + $ref: '#/components/schemas/OrderList' + '401': + $ref: '#/components/responses/Unauthorized' + /orders/{public_id}/: + get: + operationId: retrieveOrder + tags: + - Orders + summary: Retrieve an order + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: An order. + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + /orders/{public_id}/cancel/: + post: + operationId: cancelOrder + tags: + - Orders + summary: Cancel an order + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: The cancelled order. + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + /orders/{public_id}/send_now/: + post: + operationId: sendOrderNow + tags: + - Orders + summary: Send an order now + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: The updated order. + content: + application/json: + schema: + $ref: '#/components/schemas/Order' +components: + schemas: + OrderList: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Order' + next: + type: string + nullable: true + previous: + type: string + nullable: true + Order: + type: object + properties: + id: + type: string + public_id: + type: string + customer_id: + type: string + place: + type: string + format: date-time + status: + type: string + description: 'Order status: unsent, pending, success, rejected, etc.' + sub_total: + type: string + shipping_total: + type: string + total: + type: string + order_merchant_id: + type: string + payment_id: + type: string + shipping_address_id: + type: string + responses: + Unauthorized: + description: Authentication failed or the API key is missing or invalid. + parameters: + PublicId: + name: public_id + in: path + required: true + schema: + type: string + description: The public identifier of the resource. + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/openapi/upstream/products.yml b/ordergroove/openapi/upstream/products.yml new file mode 100644 index 0000000..86ad2b6 --- /dev/null +++ b/ordergroove/openapi/upstream/products.yml @@ -0,0 +1,107 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers Products API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Products + description: Product catalog and product groups. +paths: + /products/: + get: + operationId: listProducts + tags: + - Products + summary: List products + responses: + '200': + description: A paginated list of products. + content: + application/json: + schema: + $ref: '#/components/schemas/ProductList' + /products/{id}/: + get: + operationId: retrieveProduct + tags: + - Products + summary: Retrieve a product + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: A product. + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + put: + operationId: updateProduct + tags: + - Products + summary: Update a product + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + responses: + '200': + description: The updated product. + content: + application/json: + schema: + $ref: '#/components/schemas/Product' +components: + schemas: + ProductList: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Product' + next: + type: string + nullable: true + previous: + type: string + nullable: true + Product: + type: object + properties: + id: + type: string + price: + type: string + external_product_id: + type: string + autoship_enabled: + type: boolean + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/openapi/upstream/subscriptions.yml b/ordergroove/openapi/upstream/subscriptions.yml new file mode 100644 index 0000000..ab87673 --- /dev/null +++ b/ordergroove/openapi/upstream/subscriptions.yml @@ -0,0 +1,223 @@ +openapi: 3.0.3 +info: + title: Ordergroove REST Customers Subscriptions API + description: 'The Ordergroove REST API operates an enterprise subscription and relationship-commerce program on top of a merchant''s eCommerce store. It is organized around a four-object data model - Customer, Subscription, Item, and Order - plus supporting resources for Products, Offers and Incentives, Payments, Addresses, and Entitlements. Two authentication scopes exist: an Application API scope for server-to-server calls using an x-api-key header (one of ten keys per store), and a Storefront API scope using an HMAC-SHA256-signed request scoped to a single customer. All traffic is HTTPS only. This document models the publicly documented REST surface at restapi.ordergroove.com; endpoint paths are drawn from the public API reference. Ordergroove is an enterprise platform sold through sales, so an account and API keys are required to call the API, but the reference is publicly readable.' + version: '1.0' + contact: + name: Ordergroove Developer + url: https://developer.ordergroove.com +servers: +- url: https://restapi.ordergroove.com + description: Production +- url: https://staging.restapi.ordergroove.com + description: Staging +security: +- apiKeyAuth: [] +tags: +- name: Subscriptions + description: Recurring subscription agreements. +paths: + /subscriptions/: + get: + operationId: listSubscriptions + tags: + - Subscriptions + summary: List subscriptions + description: Lists subscriptions, filterable by customer, product, shipping address, live status, and created/updated date ranges. Listing across more than one customer requires the Bulk Operations permission. + parameters: + - name: customer + in: query + schema: + type: string + description: Filter by customer ID (Application scope only). + - name: product + in: query + schema: + type: string + - name: live + in: query + schema: + type: boolean + - name: created_start + in: query + schema: + type: string + format: date + - name: created_end + in: query + schema: + type: string + format: date + responses: + '200': + description: A paginated list of subscriptions. + content: + application/json: + schema: + $ref: '#/components/schemas/SubscriptionList' + '401': + $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/TooManyRequests' + /subscriptions/{public_id}/: + get: + operationId: retrieveSubscription + tags: + - Subscriptions + summary: Retrieve a subscription + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: A subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' + '401': + $ref: '#/components/responses/Unauthorized' + put: + operationId: updateSubscription + tags: + - Subscriptions + summary: Update a subscription + parameters: + - $ref: '#/components/parameters/PublicId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' + responses: + '200': + description: The updated subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' + /subscriptions/{public_id}/cancel/: + post: + operationId: cancelSubscription + tags: + - Subscriptions + summary: Cancel a subscription + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: The cancelled subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' + /subscriptions/{public_id}/reactivate/: + post: + operationId: reactivateSubscription + tags: + - Subscriptions + summary: Reactivate a subscription + parameters: + - $ref: '#/components/parameters/PublicId' + responses: + '200': + description: The reactivated subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' + /subscriptions/{public_id}/change_frequency/: + post: + operationId: changeSubscriptionFrequency + tags: + - Subscriptions + summary: Change subscription frequency + parameters: + - $ref: '#/components/parameters/PublicId' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + every: + type: integer + every_period: + type: string + enum: + - day + - week + - month + - year + responses: + '200': + description: The updated subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Subscription' +components: + schemas: + Subscription: + type: object + properties: + id: + type: string + public_id: + type: string + customer_id: + type: string + product_id: + type: string + quantity: + type: integer + payment_id: + type: string + shipping_address_id: + type: string + offer_id: + type: string + every: + type: integer + every_period: + type: string + enum: + - day + - week + - month + - year + live: + type: boolean + SubscriptionList: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Subscription' + next: + type: string + nullable: true + previous: + type: string + nullable: true + parameters: + PublicId: + name: public_id + in: path + required: true + schema: + type: string + description: The public identifier of the resource. + responses: + Unauthorized: + description: Authentication failed or the API key is missing or invalid. + TooManyRequests: + description: Rate limit exceeded (more than 6000 requests per IP per minute). Safe to retry. + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key + description: Application API scope. Send one of the ten store API keys in the x-api-key header for server-to-server requests. Storefront requests use an HMAC-SHA256 signature scoped to a single customer instead (out of band of this scheme). diff --git a/ordergroove/orders/.gitignore b/ordergroove/orders/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/orders/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/orders/counterfact-types/cookie-options.ts b/ordergroove/orders/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/orders/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/orders/counterfact-types/counterfact-response.ts b/ordergroove/orders/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/orders/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/orders/counterfact-types/example-names.ts b/ordergroove/orders/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/orders/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/orders/counterfact-types/example.ts b/ordergroove/orders/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/orders/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/orders/counterfact-types/generic-response-builder.ts b/ordergroove/orders/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/orders/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/orders/counterfact-types/http-status-code.ts b/ordergroove/orders/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/orders/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/orders/counterfact-types/if-has-key.ts b/ordergroove/orders/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/orders/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/orders/counterfact-types/index.ts b/ordergroove/orders/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/orders/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/orders/counterfact-types/maybe-promise.ts b/ordergroove/orders/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/orders/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/orders/counterfact-types/media-type.ts b/ordergroove/orders/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/orders/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/orders/counterfact-types/omit-all.ts b/ordergroove/orders/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/orders/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/orders/counterfact-types/omit-value-when-never.ts b/ordergroove/orders/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/orders/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/orders/counterfact-types/open-api-content.ts b/ordergroove/orders/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/orders/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/orders/counterfact-types/open-api-header.ts b/ordergroove/orders/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/orders/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/orders/counterfact-types/open-api-operation.ts b/ordergroove/orders/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/orders/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/orders/counterfact-types/open-api-parameters.ts b/ordergroove/orders/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/orders/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/orders/counterfact-types/open-api-response.ts b/ordergroove/orders/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/orders/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/orders/counterfact-types/random-function.ts b/ordergroove/orders/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/orders/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/orders/counterfact-types/response-builder-factory.ts b/ordergroove/orders/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/orders/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/orders/counterfact-types/response-builder.ts b/ordergroove/orders/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/orders/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/orders/counterfact-types/wide-operation-argument.ts b/ordergroove/orders/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/orders/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/orders/counterfact-types/wide-response-builder.ts b/ordergroove/orders/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/orders/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/orders/routes/_.context.ts b/ordergroove/orders/routes/_.context.ts new file mode 100644 index 0000000..ba38e00 --- /dev/null +++ b/ordergroove/orders/routes/_.context.ts @@ -0,0 +1,83 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Order } from "../types/components/schemas/Order.js"; + +export type OrderFilters = { + customer?: string; + status?: string; +}; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #orders = new Map(); + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedOrders(orders: Order[]): void { + this.#orders.clear(); + + for (const order of orders) { + if (order.public_id) { + this.#orders.set(order.public_id, structuredClone(order)); + } + } + } + + listOrders(filters: OrderFilters): Order[] { + return [...this.#orders.values()] + .filter( + (order) => + (!filters.customer || order.customer_id === filters.customer) && + (!filters.status || order.status === filters.status), + ) + .map((order) => structuredClone(order)); + } + + getOrder(publicId: string): Order | undefined { + const order = this.#orders.get(publicId); + return order ? structuredClone(order) : undefined; + } + + cancelOrder(publicId: string): Order | undefined { + return this.#updateOrder(publicId, { status: "cancelled" }); + } + + sendOrderNow( + publicId: string, + placedAt = new Date().toISOString(), + ): Order | undefined { + return this.#updateOrder(publicId, { + place: placedAt, + status: "pending", + }); + } + + #updateOrder( + publicId: string, + changes: Pick, + ): Order | undefined { + const existing = this.#orders.get(publicId); + if (!existing) return undefined; + + const order = { ...existing, ...changes }; + this.#orders.set(publicId, order); + return structuredClone(order); + } +} diff --git a/ordergroove/orders/routes/_.middleware.ts b/ordergroove/orders/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/orders/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/orders/routes/orders.ts b/ordergroove/orders/routes/orders.ts new file mode 100644 index 0000000..cd015c2 --- /dev/null +++ b/ordergroove/orders/routes/orders.ts @@ -0,0 +1,9 @@ +import type { listOrders } from "../types/paths/orders.types.js"; + +export const GET: listOrders = async ($) => { + return $.response[200].json({ + results: $.context.listOrders($.query), + next: null, + previous: null, + } as never); +}; diff --git a/ordergroove/orders/routes/orders/{public_id}.ts b/ordergroove/orders/routes/orders/{public_id}.ts new file mode 100644 index 0000000..adc877d --- /dev/null +++ b/ordergroove/orders/routes/orders/{public_id}.ts @@ -0,0 +1,8 @@ +import type { retrieveOrder } from "../../types/paths/orders/{public_id}.types.js"; + +export const GET: retrieveOrder = async ($) => { + const order = $.context.getOrder($.path.public_id); + return order + ? $.response[200].json(order) + : $.x.response[404].json({ error: "Order not found" }); +}; diff --git a/ordergroove/orders/routes/orders/{public_id}/cancel.ts b/ordergroove/orders/routes/orders/{public_id}/cancel.ts new file mode 100644 index 0000000..fb5fad7 --- /dev/null +++ b/ordergroove/orders/routes/orders/{public_id}/cancel.ts @@ -0,0 +1,8 @@ +import type { cancelOrder } from "../../../types/paths/orders/{public_id}/cancel.types.js"; + +export const POST: cancelOrder = async ($) => { + const order = $.context.cancelOrder($.path.public_id); + return order + ? $.response[200].json(order) + : $.x.response[404].json({ error: "Order not found" }); +}; diff --git a/ordergroove/orders/routes/orders/{public_id}/send_now.ts b/ordergroove/orders/routes/orders/{public_id}/send_now.ts new file mode 100644 index 0000000..5c0493f --- /dev/null +++ b/ordergroove/orders/routes/orders/{public_id}/send_now.ts @@ -0,0 +1,8 @@ +import type { sendOrderNow } from "../../../types/paths/orders/{public_id}/send_now.types.js"; + +export const POST: sendOrderNow = async ($) => { + const order = $.context.sendOrderNow($.path.public_id); + return order + ? $.response[200].json(order) + : $.x.response[404].json({ error: "Order not found" }); +}; diff --git a/ordergroove/orders/scenarios/index.ts b/ordergroove/orders/scenarios/index.ts new file mode 100644 index 0000000..108ad84 --- /dev/null +++ b/ordergroove/orders/scenarios/index.ts @@ -0,0 +1,83 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedOrders([ + { + id: "order-internal-001", + public_id: "order-001", + customer_id: "customer-001", + place: "2026-03-15T12:00:00Z", + status: "unsent", + sub_total: "25.00", + shipping_total: "2.00", + total: "27.00", + order_merchant_id: "merchant-order-001", + payment_id: "payment-001", + shipping_address_id: "address-001", + }, + { + id: "order-internal-002", + public_id: "order-002", + customer_id: "customer-002", + place: "2026-02-20T12:00:00Z", + status: "success", + sub_total: "40.00", + shipping_total: "2.00", + total: "42.00", + order_merchant_id: "merchant-order-002", + payment_id: "payment-002", + shipping_address_id: "address-002", + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/orders/test/context.test.ts b/ordergroove/orders/test/context.test.ts new file mode 100644 index 0000000..1737e24 --- /dev/null +++ b/ordergroove/orders/test/context.test.ts @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +const seed = (context: Context) => { + context.seedOrders([ + { + id: "order-internal-001", + public_id: "order-001", + customer_id: "customer-001", + place: "2026-03-15T12:00:00Z", + status: "unsent", + total: "27.00", + }, + { + id: "order-internal-002", + public_id: "order-002", + customer_id: "customer-002", + place: "2026-02-20T12:00:00Z", + status: "success", + total: "42.00", + }, + ]); +}; + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds, lists, and retrieves orders without exposing mutable state", () => { + const context = createContext(); + seed(context); + + const listed = context.listOrders({}); + assert.equal(listed.length, 2); + assert.equal(context.getOrder("order-001")?.total, "27.00"); + + listed[0]!.total = "0.00"; + assert.equal(context.getOrder("order-001")?.total, "27.00"); +}); + +test("filters orders by customer and status", () => { + const context = createContext(); + seed(context); + + assert.deepEqual( + context + .listOrders({ customer: "customer-001" }) + .map(({ public_id }) => public_id), + ["order-001"], + ); + assert.deepEqual( + context.listOrders({ status: "success" }).map(({ public_id }) => public_id), + ["order-002"], + ); + assert.deepEqual( + context.listOrders({ customer: "customer-001", status: "success" }), + [], + ); +}); + +test("cancels an order persistently", () => { + const context = createContext(); + seed(context); + + assert.equal(context.cancelOrder("order-001")?.status, "cancelled"); + assert.equal(context.getOrder("order-001")?.status, "cancelled"); + assert.equal(context.cancelOrder("missing"), undefined); +}); + +test("sends an order now by persisting its immediate place and pending status", () => { + const context = createContext(); + seed(context); + + const sent = context.sendOrderNow("order-001", "2026-03-01T10:30:00.000Z"); + + assert.equal(sent?.place, "2026-03-01T10:30:00.000Z"); + assert.equal(sent?.status, "pending"); + assert.deepEqual(context.getOrder("order-001"), sent); + assert.equal(context.sendOrderNow("missing"), undefined); +}); diff --git a/ordergroove/orders/test/routes.test.ts b/ordergroove/orders/test/routes.test.ts new file mode 100644 index 0000000..aaa8c0e --- /dev/null +++ b/ordergroove/orders/test/routes.test.ts @@ -0,0 +1,192 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/orders.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/orders/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key for collection, detail, and actions", async () => { + for (const [pathname, method] of [ + ["/orders/", "GET"], + ["/orders/order-001/", "GET"], + ["/orders/order-001/cancel/", "POST"], + ["/orders/order-001/send_now/", "POST"], + ] as const) { + const response = await fetch(`http://127.0.0.1:${port}${pathname}`, { + method, + }); + assert.equal(response.status, 401, pathname); + assert.deepEqual(await response.json(), { error: "Unauthorized" }); + } +}); + +test("lists deterministic orders and filters by customer and status", async () => { + const response = await request("/orders/"); + assert.equal(response.status, 200); + const body = await response.json(); + assert.deepEqual( + body.results.map(({ public_id }: { public_id: string }) => public_id), + ["order-001", "order-002"], + ); + assert.equal(body.results[0].customer_id, "customer-001"); + assert.equal(body.results[1].customer_id, "customer-002"); + + for (const [query, expected] of [ + ["customer=customer-001", ["order-001"]], + ["status=success", ["order-002"]], + ["customer=customer-001&status=success", []], + ] as const) { + const filtered = await request(`/orders/?${query}`); + assert.equal(filtered.status, 200); + assert.deepEqual( + (await filtered.json()).results.map( + ({ public_id }: { public_id: string }) => public_id, + ), + expected, + query, + ); + } +}); + +test("retrieves an order", async () => { + const response = await request("/orders/order-001/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + id: "order-internal-001", + public_id: "order-001", + customer_id: "customer-001", + place: "2026-03-15T12:00:00Z", + status: "unsent", + sub_total: "25.00", + shipping_total: "2.00", + total: "27.00", + order_merchant_id: "merchant-order-001", + payment_id: "payment-001", + shipping_address_id: "address-001", + }); +}); + +test("cancels an order and persists the transition", async () => { + const response = await request("/orders/order-002/cancel/", { + method: "POST", + }); + assert.equal(response.status, 200); + assert.equal((await response.json()).status, "cancelled"); + + const persisted = await request("/orders/order-002/"); + assert.equal(persisted.status, 200); + assert.equal((await persisted.json()).status, "cancelled"); +}); + +test("sends an order now and persists its immediate pending state", async () => { + const response = await request("/orders/order-001/send_now/", { + method: "POST", + }); + assert.equal(response.status, 200); + const sent = await response.json(); + assert.equal(sent.status, "pending"); + assert.notEqual(sent.place, "2026-03-15T12:00:00Z"); + assert.equal(Number.isNaN(Date.parse(sent.place)), false); + + const persisted = await request("/orders/order-001/"); + assert.equal(persisted.status, 200); + assert.equal((await persisted.json()).place, sent.place); +}); + +test("returns 404 for every operation on unknown orders", async () => { + for (const [pathname, method] of [ + ["/orders/not-found/", "GET"], + ["/orders/not-found/cancel/", "POST"], + ["/orders/not-found/send_now/", "POST"], + ] as const) { + const response = await request(pathname, { method }); + assert.equal(response.status, 404, pathname); + assert.deepEqual(await response.json(), { error: "Order not found" }); + } +}); diff --git a/ordergroove/orders/types/#/components/responses/Unauthorized.ts b/ordergroove/orders/types/#/components/responses/Unauthorized.ts new file mode 100644 index 0000000..d6753c8 --- /dev/null +++ b/ordergroove/orders/types/#/components/responses/Unauthorized.ts @@ -0,0 +1,6 @@ +export type Unauthorized = { + headers: never; + requiredHeaders: never; + content: never; + examples: {}; +}; diff --git a/ordergroove/orders/types/_.context.ts b/ordergroove/orders/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/orders/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/orders/types/components/schemas/Order.ts b/ordergroove/orders/types/components/schemas/Order.ts new file mode 100644 index 0000000..feb2e91 --- /dev/null +++ b/ordergroove/orders/types/components/schemas/Order.ts @@ -0,0 +1,19 @@ +export type Order = { + id?: string; + public_id?: string; + customer_id?: string; + /** + * @format date-time + */ + place?: string; + /** + * Order status: unsent, pending, success, rejected, etc. + */ + status?: string; + sub_total?: string; + shipping_total?: string; + total?: string; + order_merchant_id?: string; + payment_id?: string; + shipping_address_id?: string; +}; diff --git a/ordergroove/orders/types/components/schemas/OrderList.ts b/ordergroove/orders/types/components/schemas/OrderList.ts new file mode 100644 index 0000000..d6c7f80 --- /dev/null +++ b/ordergroove/orders/types/components/schemas/OrderList.ts @@ -0,0 +1,7 @@ +import type { Order } from "./Order.js"; + +export type OrderList = { + results?: Array; + next?: string; + previous?: string; +}; diff --git a/ordergroove/orders/types/paths/orders.types.ts b/ordergroove/orders/types/paths/orders.types.ts new file mode 100644 index 0000000..c26807a --- /dev/null +++ b/ordergroove/orders/types/paths/orders.types.ts @@ -0,0 +1,50 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { OrderList } from "../components/schemas/OrderList.js"; +import type { Unauthorized } from "../#/components/responses/Unauthorized.js"; + +/** + * List orders + */ +export type listOrders = ( + $: OmitValueWhenNever<{ + query: listOrders_Query; + querystring: never; + path: never; + headers: listOrders_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: OrderList; + }; + }; + examples: {}; + }; + 401: Unauthorized; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listOrders_Query = { customer?: string; status?: string }; + +export type listOrders_Headers = { "x-api-key": string }; diff --git a/ordergroove/orders/types/paths/orders/{public_id}.types.ts b/ordergroove/orders/types/paths/orders/{public_id}.types.ts new file mode 100644 index 0000000..88ed4b1 --- /dev/null +++ b/ordergroove/orders/types/paths/orders/{public_id}.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts"; +import type { Context } from "../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts"; +import type { Order } from "../../components/schemas/Order.js"; + +/** + * Retrieve an order + */ +export type retrieveOrder = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: retrieveOrder_Path; + headers: retrieveOrder_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Order; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type retrieveOrder_Path = { public_id: string }; + +export type retrieveOrder_Headers = { "x-api-key": string }; diff --git a/ordergroove/orders/types/paths/orders/{public_id}/cancel.types.ts b/ordergroove/orders/types/paths/orders/{public_id}/cancel.types.ts new file mode 100644 index 0000000..866794b --- /dev/null +++ b/ordergroove/orders/types/paths/orders/{public_id}/cancel.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.ts"; +import type { Context } from "../../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts"; +import type { Order } from "../../../components/schemas/Order.js"; + +/** + * Cancel an order + */ +export type cancelOrder = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: cancelOrder_Path; + headers: cancelOrder_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Order; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type cancelOrder_Path = { public_id: string }; + +export type cancelOrder_Headers = { "x-api-key": string }; diff --git a/ordergroove/orders/types/paths/orders/{public_id}/send_now.types.ts b/ordergroove/orders/types/paths/orders/{public_id}/send_now.types.ts new file mode 100644 index 0000000..3718e0d --- /dev/null +++ b/ordergroove/orders/types/paths/orders/{public_id}/send_now.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.ts"; +import type { Context } from "../../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts"; +import type { Order } from "../../../components/schemas/Order.js"; + +/** + * Send an order now + */ +export type sendOrderNow = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: sendOrderNow_Path; + headers: sendOrderNow_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Order; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type sendOrderNow_Path = { public_id: string }; + +export type sendOrderNow_Headers = { "x-api-key": string }; diff --git a/ordergroove/package-lock.json b/ordergroove/package-lock.json new file mode 100644 index 0000000..d44c46a --- /dev/null +++ b/ordergroove/package-lock.json @@ -0,0 +1,4949 @@ +{ + "name": "@counterfact/ordergroove", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@counterfact/ordergroove", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "counterfact": "2.14.2" + }, + "devDependencies": { + "eslint": "^9.39.4", + "prettier": "^3.8.3", + "typescript-eslint": "^8.60.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.5.0.tgz", + "integrity": "sha512-Ps4w0FwrDoeVK6hfYxWkVbkmxm+zN+6xoXF2ZfEhfiox0ZNbcSAiUWO6iAIvP5bc3DB270r+EaKcoT1IUyzfxw==", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dependents/detective-less": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-6.0.1.tgz", + "integrity": "sha512-WRFmqQuZBajKzksO3kWCxskitTqxTdZ/xkyWn7gX+jvnmEeZauH8ngLs5RiluZZrXBNzngJd7kTrN0QwVhgAHg==", + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@posthog/core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.46.1.tgz", + "integrity": "sha512-EoCFduRkvrg9E5ylMi4QnZCjlAdRJCq6tJouWfngBVR79XSI4iPvIWYA+CdzokAjk+TfSVBFVJ++4Im3r+T0Dg==", + "license": "MIT", + "dependencies": { + "@posthog/types": "^1.399.0" + } + }, + "node_modules/@posthog/types": { + "version": "1.399.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.399.0.tgz", + "integrity": "sha512-/WDwBzqIPko8VJ1B+0rlso2XQEz9+2sqtsY9Tqy3p1GhgTqsFakcz/PmMpAnA321LTEZVRcO6x5hAwABV4yrDw==", + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/koa": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-3.0.3.tgz", + "integrity": "sha512-TdtNEJ7sYSrFQcVuS2ySsVqnq5EyE3oJbnfFJvkC9UtGP4Kpem5KE7r+ivHIbIAQAofSqnlB5D3vkfYO69TQpg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "^2", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-module-types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-7.0.0.tgz", + "integrity": "sha512-WZf/zDJlZYMkn+/MSl1uwJzUnDzTpVitUCcvX2oUZzN5ccYTyiiI05249AYCOqMjvbKuwiielm7GfoQKYpH/7A==", + "license": "MIT", + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/co-body/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "license": "MIT" + }, + "node_modules/counterfact": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/counterfact/-/counterfact-2.14.2.tgz", + "integrity": "sha512-Ik6vZCOoev4fMp/pfXnLUF672yvWgm4qv/MW7XLNIVJXP/lq6kiq+APvI4JpZX00CSxhtid7/fcsxqPlInI7jw==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "15.5.0", + "@hapi/accept": "6.0.3", + "@types/json-schema": "7.0.15", + "ajv": "8.20.0", + "chokidar": "5.0.0", + "commander": "15.0.0", + "debug": "4.4.3", + "fs-extra": "11.4.0", + "http-terminator": "3.2.0", + "js-yaml": "5.2.2", + "json-schema-faker": "0.6.2", + "jsonpath-plus": "10.4.0", + "jsonwebtoken": "9.0.3", + "koa": "3.2.1", + "koa-bodyparser": "4.4.1", + "koa-proxies": "0.12.4", + "koa2-swagger-ui": "5.12.0", + "node-fetch": "3.3.2", + "open": "11.0.0", + "posthog-node": "5.46.0", + "precinct": "13.0.1", + "prettier": "3.8.5", + "recast": "0.23.12", + "tsx": "4.23.1", + "typescript": "6.0.3" + }, + "bin": { + "counterfact": "bin/counterfact.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/pmcelhaney" + } + }, + "node_modules/counterfact/node_modules/prettier": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.5.tgz", + "integrity": "sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detective-amd": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-7.0.0.tgz", + "integrity": "sha512-ZSbF4yyY2P4VfXmyKfVt6YuHYXu5CE21iSpMzZq2ItcmVeVR90emURHO6bPSlCq/TfWo/NCTWJtA5wlnTBQ/UA==", + "license": "MIT", + "dependencies": { + "ast-module-types": "^7.0.0", + "escodegen": "^2.1.0", + "get-amd-module-type": "^7.0.0", + "node-source-walk": "^8.0.0" + }, + "bin": { + "detective-amd": "bin/cli.js" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-cjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-7.0.0.tgz", + "integrity": "sha512-rp+2gWa939K35U6UVZc7af6RkJlUTsoDLsJS/1wPTCKUSt/E2rEnLHbE01DPJGsXzDso0hLfzdePwK0935EwPg==", + "license": "MIT", + "dependencies": { + "ast-module-types": "^7.0.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-es6": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-6.0.0.tgz", + "integrity": "sha512-gbrrUDI8a3VvQilbdlZLC1IOUdydtjIgoNF1dEUsy0Rba+ianG2tC5d5mXAp0T4GrJuzjyzP4yZVAtcUx6YYNg==", + "license": "MIT", + "dependencies": { + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-postcss": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-9.0.1.tgz", + "integrity": "sha512-rTAmnRsU28hpuYn1317ddubhi9i2zreiKpQlzddHQNgliiwG9AjAyoRDEUt+9Y6wlH/QQgdKmXWQw7VTv0zJmQ==", + "license": "MIT", + "dependencies": { + "is-url-superb": "^4.0.0", + "postcss-values-parser": "^6.0.2" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "postcss": "^8.4.47" + } + }, + "node_modules/detective-sass": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-7.0.1.tgz", + "integrity": "sha512-iv9U1j8C5AfVV4pHCCpU+a0H40pv6Xz+n0Hxi64ZHRjbJ3SU+WuS+ZW0V6uASL/L5xDi7Nz+UB8T9ZAK0epMxw==", + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-scss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-6.0.1.tgz", + "integrity": "sha512-ZPGAJjRrSo7V84MvlzlkriX9VRsA0ln8kIRg5sRH9fi5V/0gWyMBcu9nPJVZag5h6IfIiJdJFaWBYz+zffw4/A==", + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-stylus": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-6.0.0.tgz", + "integrity": "sha512-H12WWqOaajhdVUnePu3s3CyqQdYFcAJhgztDWDqu2giechAGPewXo0KctAd/aHM9bP+mpYvMHty6iAjzjcQNLg==", + "license": "MIT", + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/detective-typescript": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-15.0.1.tgz", + "integrity": "sha512-7+5ZwOkvIrIxKDMxcoiW2NtbBr6IM6BzORVrpx3nKmNin4Q445ev9J7dqEfYB4RMVsAcbVLlwI2QD17YIUlP3A==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "^8.62.1", + "ast-module-types": "^7.0.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "typescript": "^5.4.4 || ^6.0.2" + } + }, + "node_modules/detective-vue2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-3.0.1.tgz", + "integrity": "sha512-5k3MvsTytl4Ci0whix9pro56toTH3wallMAj8X9FLG3d0NuZ9WJn1u55A6ltBWrDaCewXl/Knr4jYGWxETuvGQ==", + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^6.0.1", + "@vue/compiler-sfc": "^3.5.35", + "detective-es6": "^6.0.0", + "detective-sass": "^7.0.1", + "detective-scss": "^6.0.1", + "detective-stylus": "^6.0.0", + "detective-typescript": "^15.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "typescript": "^5.4.4 || ^6.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-printf": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz", + "integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-amd-module-type": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-7.0.0.tgz", + "integrity": "sha512-Pxu8tqdNbCBWoahaWS1EElwWxP9iNVTWr3VlqsobQPTs5qs77E66BI79ln00d2QoJdcvKIcoDlKDOiVGBehoDA==", + "license": "MIT", + "dependencies": { + "ast-module-types": "^7.0.0", + "node-source-walk": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-terminator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz", + "integrity": "sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==", + "license": "BSD-3-Clause", + "dependencies": { + "delay": "^5.0.0", + "p-wait-for": "^3.2.0", + "roarr": "^7.0.4", + "type-fest": "^2.3.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-faker": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.6.2.tgz", + "integrity": "sha512-jhOV/bIUxTPM3DiKaa9YDI9UIhO6md1wBnopuIleXATBCwBxnpEfUG0KZqWJS0nTc7nGeWu6ve4vL7Sk7l2BKA==", + "license": "MIT", + "bin": { + "jsf": "dist/bin/cli.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/koa": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.2.1.tgz", + "integrity": "sha512-e7IpWJrnanNUroVK2taAgMxoEZvHLXdQiNjeExSu/DEIWm83jaKGBgb7tLmu2rMYpA027qFB3iLR/k3AVpFRnA==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.8", + "content-disposition": "~1.0.1", + "content-type": "^1.0.5", + "cookies": "~0.9.1", + "delegates": "^1.0.0", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/koa-bodyparser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.4.1.tgz", + "integrity": "sha512-kBH3IYPMb+iAXnrxIhXnW+gXV8OTzCu8VPDqvcDHW9SQrbkHmqPQtiZwrltNmSq6/lpipHnT7k7PsjlVD7kK0w==", + "license": "MIT", + "dependencies": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/koa-bodyparser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa-proxies": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/koa-proxies/-/koa-proxies-0.12.4.tgz", + "integrity": "sha512-xxrEtN0e7s7/gNRoOMUltCbuIaCWqTQUTZNWQqet/8MoxSW0hG422lx2Al9FfYO3nCeA+b5c5/YmILRzavivDA==", + "license": "MIT", + "dependencies": { + "http-proxy": "^1.18.1", + "path-match": "^1.2.4", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "koa": ">=2" + } + }, + "node_modules/koa2-swagger-ui": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/koa2-swagger-ui/-/koa2-swagger-ui-5.12.0.tgz", + "integrity": "sha512-F7nRhiFbGcKq5OwhGMRGZdEckNW9ldreaZifDIv/bZYghtR632wo25rLBbsLHQHVb89V4vu/drLiCIJIWQIw+w==", + "license": "MIT", + "dependencies": { + "handlebars": "^4.7.8", + "lodash": "^4.17.21", + "read-pkg-up": "7.0.1" + }, + "peerDependencies": { + "@types/koa": "*" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-definition": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-7.0.0.tgz", + "integrity": "sha512-ctl9cJHEHUqP8IehBhEq9NuykuFVJAhxeLR4BeObsrcwrZMTu2ax3F22Ii5bad1T41LwKOug/9FAzO9wmfAZ1Q==", + "license": "MIT", + "dependencies": { + "ast-module-types": "^7.0.0", + "node-source-walk": "^8.0.0" + }, + "bin": { + "module-definition": "bin/cli.js" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-source-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-8.0.0.tgz", + "integrity": "sha512-/CsHIdZsVgpGm4A7cD+Vdajv2GyqkVPUNtpChugutV/O9e3j1YWkdOWwhWKo0NfRM4r4XoiKtkb4JoLzsCN5vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", + "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", + "license": "MIT", + "dependencies": { + "p-timeout": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-match": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/path-match/-/path-match-1.2.4.tgz", + "integrity": "sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", + "dependencies": { + "http-errors": "~1.4.0", + "path-to-regexp": "^1.0.0" + } + }, + "node_modules/path-match/node_modules/http-errors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz", + "integrity": "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.1", + "statuses": ">= 1.2.1 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/path-match/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", + "license": "ISC" + }, + "node_modules/path-match/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, + "node_modules/posthog-node": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.46.0.tgz", + "integrity": "sha512-Uzkth327Qxho9X55UygGUjVKCF9oaox90HQpa0o9YNjwLjbQmXttgHChzAtjAcsMw/ZKr3NnHC3xcAaS5dXwEQ==", + "license": "MIT", + "dependencies": { + "@posthog/core": "^1.44.0" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/precinct": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-13.0.1.tgz", + "integrity": "sha512-mmrpDzb6tSF33WsEi7hds6xJlr85eU0nuhkiYR6PBwcgdvp5fPZ4FNx8c2IqlmZqrG5+wgwRTDfaZXbfWuyWmA==", + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^6.0.1", + "commander": "^14.0.3", + "detective-amd": "^7.0.0", + "detective-cjs": "^7.0.0", + "detective-es6": "^6.0.0", + "detective-postcss": "^9.0.1", + "detective-sass": "^7.0.1", + "detective-scss": "^6.0.1", + "detective-stylus": "^6.0.0", + "detective-typescript": "^15.0.1", + "detective-vue2": "^3.0.1", + "module-definition": "^7.0.0", + "node-source-walk": "^8.0.0", + "postcss": "^8.5.19", + "typescript": "^6.0.3" + }, + "bin": { + "precinct": "bin/cli.js" + }, + "engines": { + "node": ">=20.19.0 || >=22.12.0" + } + }, + "node_modules/precinct/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recast": { + "version": "0.23.12", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", + "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/roarr": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-7.21.7.tgz", + "integrity": "sha512-6X9ID9jb83kPkDxm5xLb9rZdGm6ksdDjpeL0iW5weKelJFi+NVBSowRpEuH8CdpX2x+7RW0TZaezibE7yQB7pg==", + "license": "BSD-3-Clause", + "dependencies": { + "fast-printf": "^1.6.9", + "safe-stable-stringify": "^2.4.3", + "semver-compare": "^1.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "license": "CC0-1.0" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ordergroove/package.json b/ordergroove/package.json new file mode 100644 index 0000000..9de5a23 --- /dev/null +++ b/ordergroove/package.json @@ -0,0 +1,24 @@ +{ + "name": "@counterfact/ordergroove", + "version": "1.0.0", + "description": "Combined Counterfact simulator package for Ordergroove's REST APIs", + "scripts": { + "start": "counterfact --config counterfact.yaml", + "generate": "counterfact --config counterfact.yaml --generate", + "serve": "counterfact --config counterfact.yaml --serve", + "lint": "eslint . && prettier --check .", + "test": "node --import tsx --test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "counterfact": "2.14.2" + }, + "devDependencies": { + "eslint": "^9.39.4", + "prettier": "^3.8.3", + "typescript-eslint": "^8.60.0" + } +} diff --git a/ordergroove/products/.gitignore b/ordergroove/products/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/products/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/products/counterfact-types/cookie-options.ts b/ordergroove/products/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/products/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/products/counterfact-types/counterfact-response.ts b/ordergroove/products/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/products/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/products/counterfact-types/example-names.ts b/ordergroove/products/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/products/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/products/counterfact-types/example.ts b/ordergroove/products/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/products/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/products/counterfact-types/generic-response-builder.ts b/ordergroove/products/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/products/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/products/counterfact-types/http-status-code.ts b/ordergroove/products/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/products/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/products/counterfact-types/if-has-key.ts b/ordergroove/products/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/products/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/products/counterfact-types/index.ts b/ordergroove/products/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/products/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/products/counterfact-types/maybe-promise.ts b/ordergroove/products/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/products/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/products/counterfact-types/media-type.ts b/ordergroove/products/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/products/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/products/counterfact-types/omit-all.ts b/ordergroove/products/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/products/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/products/counterfact-types/omit-value-when-never.ts b/ordergroove/products/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/products/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/products/counterfact-types/open-api-content.ts b/ordergroove/products/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/products/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/products/counterfact-types/open-api-header.ts b/ordergroove/products/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/products/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/products/counterfact-types/open-api-operation.ts b/ordergroove/products/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/products/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/products/counterfact-types/open-api-parameters.ts b/ordergroove/products/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/products/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/products/counterfact-types/open-api-response.ts b/ordergroove/products/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/products/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/products/counterfact-types/random-function.ts b/ordergroove/products/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/products/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/products/counterfact-types/response-builder-factory.ts b/ordergroove/products/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/products/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/products/counterfact-types/response-builder.ts b/ordergroove/products/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/products/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/products/counterfact-types/wide-operation-argument.ts b/ordergroove/products/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/products/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/products/counterfact-types/wide-response-builder.ts b/ordergroove/products/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/products/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/products/routes/_.context.ts b/ordergroove/products/routes/_.context.ts new file mode 100644 index 0000000..752293b --- /dev/null +++ b/ordergroove/products/routes/_.context.ts @@ -0,0 +1,55 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Product } from "../types/components/schemas/Product.js"; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #products = new Map(); + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedProducts(products: Product[]): void { + this.#products.clear(); + for (const product of products) { + if (product.id) { + this.#products.set(product.id, structuredClone(product)); + } + } + } + + listProducts(): Product[] { + return [...this.#products.values()].map((product) => + structuredClone(product), + ); + } + + getProduct(id: string): Product | undefined { + const product = this.#products.get(id); + return product ? structuredClone(product) : undefined; + } + + replaceProduct(id: string, input: Product): Product | undefined { + if (!this.#products.has(id)) return undefined; + + const product = { ...structuredClone(input), id }; + this.#products.set(id, product); + return structuredClone(product); + } +} diff --git a/ordergroove/products/routes/_.middleware.ts b/ordergroove/products/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/products/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/products/routes/products.ts b/ordergroove/products/routes/products.ts new file mode 100644 index 0000000..f793328 --- /dev/null +++ b/ordergroove/products/routes/products.ts @@ -0,0 +1,9 @@ +import type { listProducts } from "../types/paths/products.types.js"; + +export const GET: listProducts = async ($) => { + return $.response[200].json({ + results: $.context.listProducts(), + next: null, + previous: null, + } as never); +}; diff --git a/ordergroove/products/routes/products/{id}.ts b/ordergroove/products/routes/products/{id}.ts new file mode 100644 index 0000000..c7807ea --- /dev/null +++ b/ordergroove/products/routes/products/{id}.ts @@ -0,0 +1,16 @@ +import type { retrieveProduct } from "../../types/paths/products/{id}.types.js"; +import type { updateProduct } from "../../types/paths/products/{id}.types.js"; + +export const GET: retrieveProduct = async ($) => { + const product = $.context.getProduct($.path.id); + return product + ? $.response[200].json(product) + : $.x.response[404].json({ error: "Product not found" }); +}; + +export const PUT: updateProduct = async ($) => { + const product = $.context.replaceProduct($.path.id, $.body); + return product + ? $.response[200].json(product) + : $.x.response[404].json({ error: "Product not found" }); +}; diff --git a/ordergroove/products/scenarios/index.ts b/ordergroove/products/scenarios/index.ts new file mode 100644 index 0000000..d79c041 --- /dev/null +++ b/ordergroove/products/scenarios/index.ts @@ -0,0 +1,69 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedProducts([ + { + id: "product-001", + price: "19.99", + external_product_id: "sku-coffee", + autoship_enabled: true, + }, + { + id: "product-002", + price: "12.50", + external_product_id: "sku-filters", + autoship_enabled: false, + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/products/test/context.test.ts b/ordergroove/products/test/context.test.ts new file mode 100644 index 0000000..62d3e74 --- /dev/null +++ b/ordergroove/products/test/context.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds, lists, and retrieves products without exposing mutable state", () => { + const context = createContext(); + context.seedProducts([ + { + id: "product-001", + price: "19.99", + external_product_id: "sku-coffee", + autoship_enabled: true, + }, + ]); + + const listed = context.listProducts(); + assert.equal(listed.length, 1); + assert.equal(context.getProduct("product-001")?.price, "19.99"); + + listed[0]!.price = "0.00"; + assert.equal(context.getProduct("product-001")?.price, "19.99"); +}); + +test("replacement persists, preserves the path identifier, and removes omitted fields", () => { + const context = createContext(); + context.seedProducts([ + { + id: "product-001", + price: "19.99", + external_product_id: "sku-coffee", + autoship_enabled: true, + }, + ]); + + const updated = context.replaceProduct("product-001", { + id: "ignored-id", + price: "21.50", + autoship_enabled: false, + }); + + assert.deepEqual(updated, { + id: "product-001", + price: "21.50", + autoship_enabled: false, + }); + assert.deepEqual(context.getProduct("product-001"), updated); + assert.equal(context.replaceProduct("missing", { price: "1.00" }), undefined); +}); diff --git a/ordergroove/products/test/routes.test.ts b/ordergroove/products/test/routes.test.ts new file mode 100644 index 0000000..4fad743 --- /dev/null +++ b/ordergroove/products/test/routes.test.ts @@ -0,0 +1,173 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/products.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/products/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key", async () => { + const missing = await fetch(`http://127.0.0.1:${port}/products/`); + assert.equal(missing.status, 401); + assert.deepEqual(await missing.json(), { error: "Unauthorized" }); + + const invalid = await request("/products/", { + headers: { "x-api-key": "invalid" }, + }); + assert.equal(invalid.status, 401); +}); + +test("lists deterministic startup products", async () => { + const response = await request("/products/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + results: [ + { + id: "product-001", + price: "19.99", + external_product_id: "sku-coffee", + autoship_enabled: true, + }, + { + id: "product-002", + price: "12.50", + external_product_id: "sku-filters", + autoship_enabled: false, + }, + ], + next: null, + previous: null, + }); +}); + +test("retrieves, replaces, and persists a product", async () => { + const retrieveResponse = await request("/products/product-001/"); + assert.equal(retrieveResponse.status, 200); + assert.equal( + (await retrieveResponse.json()).external_product_id, + "sku-coffee", + ); + + const updateResponse = await request("/products/product-001/", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + id: "ignored-id", + price: "21.50", + autoship_enabled: false, + }), + }); + assert.equal(updateResponse.status, 200); + assert.deepEqual(await updateResponse.json(), { + id: "product-001", + price: "21.50", + autoship_enabled: false, + }); + + const persistedResponse = await request("/products/product-001/"); + assert.equal(persistedResponse.status, 200); + assert.deepEqual(await persistedResponse.json(), { + id: "product-001", + price: "21.50", + autoship_enabled: false, + }); +}); + +test("returns 404 for unknown products", async () => { + const missingGet = await request("/products/not-found/"); + assert.equal(missingGet.status, 404); + assert.deepEqual(await missingGet.json(), { error: "Product not found" }); + + const missingPut = await request("/products/not-found/", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ price: "1.00" }), + }); + assert.equal(missingPut.status, 404); + assert.deepEqual(await missingPut.json(), { error: "Product not found" }); +}); diff --git a/ordergroove/products/types/_.context.ts b/ordergroove/products/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/products/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/products/types/components/schemas/Product.ts b/ordergroove/products/types/components/schemas/Product.ts new file mode 100644 index 0000000..797593f --- /dev/null +++ b/ordergroove/products/types/components/schemas/Product.ts @@ -0,0 +1,6 @@ +export type Product = { + id?: string; + price?: string; + external_product_id?: string; + autoship_enabled?: boolean; +}; diff --git a/ordergroove/products/types/components/schemas/ProductList.ts b/ordergroove/products/types/components/schemas/ProductList.ts new file mode 100644 index 0000000..5811711 --- /dev/null +++ b/ordergroove/products/types/components/schemas/ProductList.ts @@ -0,0 +1,7 @@ +import type { Product } from "./Product.js"; + +export type ProductList = { + results?: Array; + next?: string; + previous?: string; +}; diff --git a/ordergroove/products/types/paths/products.types.ts b/ordergroove/products/types/paths/products.types.ts new file mode 100644 index 0000000..9a3aac3 --- /dev/null +++ b/ordergroove/products/types/paths/products.types.ts @@ -0,0 +1,46 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { ProductList } from "../components/schemas/ProductList.js"; + +/** + * List products + */ +export type listProducts = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: never; + headers: listProducts_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: ProductList; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listProducts_Headers = { "x-api-key": string }; diff --git a/ordergroove/products/types/paths/products/{id}.types.ts b/ordergroove/products/types/paths/products/{id}.types.ts new file mode 100644 index 0000000..31bc677 --- /dev/null +++ b/ordergroove/products/types/paths/products/{id}.types.ts @@ -0,0 +1,85 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts"; +import type { Context } from "../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts"; +import type { Product } from "../../components/schemas/Product.js"; + +/** + * Retrieve a product + */ +export type retrieveProduct = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: retrieveProduct_Path; + headers: retrieveProduct_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Product; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Update a product + */ +export type updateProduct = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: updateProduct_Path; + headers: updateProduct_Headers; + cookie: never; + body: Product; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Product; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type retrieveProduct_Path = { id: string }; + +export type retrieveProduct_Headers = { "x-api-key": string }; + +export type updateProduct_Path = { id: string }; + +export type updateProduct_Headers = { "x-api-key": string }; diff --git a/ordergroove/subscriptions/.gitignore b/ordergroove/subscriptions/.gitignore new file mode 100644 index 0000000..16d3c4d --- /dev/null +++ b/ordergroove/subscriptions/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/ordergroove/subscriptions/counterfact-types/cookie-options.ts b/ordergroove/subscriptions/counterfact-types/cookie-options.ts new file mode 100644 index 0000000..2bed81d --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/cookie-options.ts @@ -0,0 +1,14 @@ +/** + * Options for setting an HTTP cookie on a response. + * These correspond to standard `Set-Cookie` attributes and are passed to the + * `.cookie()` method on the response builder. + */ +export interface CookieOptions { + domain?: string; + expires?: Date; + httpOnly?: boolean; + maxAge?: number; + path?: string; + sameSite?: "lax" | "none" | "strict"; + secure?: boolean; +} diff --git a/ordergroove/subscriptions/counterfact-types/counterfact-response.ts b/ordergroove/subscriptions/counterfact-types/counterfact-response.ts new file mode 100644 index 0000000..9488ff7 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/counterfact-response.ts @@ -0,0 +1,15 @@ +/** + * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type. + * This prevents arbitrary objects from being accidentally treated as a + * completed response value. + */ +const counterfactResponse = Symbol("Counterfact Response"); + +/** + * The terminal value type returned by the fluent response builder once all + * required fields (body, headers, etc.) have been provided. When a route + * handler returns this type, Counterfact treats the response as complete. + */ +export type COUNTERFACT_RESPONSE = { + [counterfactResponse]: typeof counterfactResponse; +}; diff --git a/ordergroove/subscriptions/counterfact-types/example-names.ts b/ordergroove/subscriptions/counterfact-types/example-names.ts new file mode 100644 index 0000000..d1fe5b3 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/example-names.ts @@ -0,0 +1,13 @@ +import type { OpenApiResponse } from "./open-api-response.js"; + +/** + * Extracts the union of named example keys defined on an OpenAPI response. + * Resolves to `never` when the response has no named examples. + * Used to constrain the argument to the `.example(name)` method on the + * response builder. + */ +export type ExampleNames = Response extends { + examples: infer E; +} + ? keyof E & string + : never; diff --git a/ordergroove/subscriptions/counterfact-types/example.ts b/ordergroove/subscriptions/counterfact-types/example.ts new file mode 100644 index 0000000..52561d6 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/example.ts @@ -0,0 +1,14 @@ +/** + * Represents a named example defined in an OpenAPI document. + * Examples can be referenced by route handlers via the `.example(name)` method + * on the response builder. + * + * OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`. + * When present, `dataValue` is preferred over `value`. + */ +export interface Example { + dataValue?: unknown; + description: string; + summary: string; + value?: unknown; +} diff --git a/ordergroove/subscriptions/counterfact-types/generic-response-builder.ts b/ordergroove/subscriptions/counterfact-types/generic-response-builder.ts new file mode 100644 index 0000000..25e28c8 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/generic-response-builder.ts @@ -0,0 +1,167 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { CookieOptions } from "./cookie-options.js"; +import type { ExampleNames } from "./example-names.js"; +import type { IfHasKey } from "./if-has-key.js"; +import type { MediaType } from "./media-type.js"; +import type { OmitAll } from "./omit-all.js"; +import type { OmitValueWhenNever } from "./omit-value-when-never.js"; +import type { OpenApiResponse } from "./open-api-response.js"; +import type { RandomFunction } from "./random-function.js"; + +/** + * Returns `never` when `Record` is an empty object type (`{}`), signalling + * that there are no remaining choices available on the response builder. + */ +type NeverIfEmpty = object extends Record ? never : Record; + +/** + * Extracts the union of schema types from a map of media-type content entries. + * Used to type the body argument of shortcut methods like `.json()` or `.html()`. + */ +type SchemasOf = { + [K in keyof T]: T[K]["schema"]; +}[keyof T]; + +/** + * Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when + * the response contains at least one of the given `ContentTypes`, and `never` + * otherwise. Calling the method narrows the builder by removing those content + * types from the remaining options. + */ +type MaybeShortcut< + ContentTypes extends MediaType[], + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentTypes, + (body: SchemasOf) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; + }>, + never +>; + +/** + * The type of the `.match(contentType, body)` method on the generic response + * builder. Calling it narrows the builder by removing the chosen content type + * from the remaining options. + */ +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty>; + headers: Response["headers"]; + requiredHeaders: Response["requiredHeaders"]; +}>; + +/** + * The type of the `.header(name, value)` method on the generic response + * builder. Calling it narrows the builder by removing the satisfied header + * from the set of required headers. + */ +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: NeverIfEmpty; + headers: NeverIfEmpty>; + requiredHeaders: Exclude; +}>; + +/** + * The inner shape of the generic response builder, listing all methods that + * are currently available given the remaining response constraints. + * Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`. + * + * Note: `[T] extends [never]` (non-distributive tuple wrapping) is used + * alongside `[keyof T] extends [never]` to correctly handle both `T = never` + * (spec-generated no-body) and `T = {}` (all content types consumed) cases. + * TypeScript evaluates `keyof never` as `string | number | symbol`, so a + * direct `[keyof never] extends [never]` check would incorrectly return false. + */ +export type GenericResponseBuilderInner< + Response extends OpenApiResponse = OpenApiResponse, +> = OmitValueWhenNever<{ + binary: MaybeShortcut<["application/octet-stream"], Response>; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => GenericResponseBuilder; + empty: [Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : [keyof Response["content"]] extends [never] + ? () => COUNTERFACT_RESPONSE + : never; + header: [Response["headers"]] extends [never] + ? never + : [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut< + [ + "application/json", + "text/json", + "text/x-json", + "application/xml", + "text/xml", + ], + Response + >; + match: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [Response["content"]] extends [never] + ? never + : [keyof Response["content"]] extends [never] + ? never + : RandomFunction; + example: [ExampleNames] extends [never] + ? never + : (name: ExampleNames) => COUNTERFACT_RESPONSE; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; + stream: MaybeShortcut< + ["text/event-stream", "application/jsonl", "application/json-seq"], + Response + >; +}>; + +/** + * The strongly-typed, fluent response builder generated for each operation in + * a route handler. Its available methods are derived from the OpenAPI response + * schema: as methods are called, the builder type narrows until all required + * content and headers have been provided, at which point it resolves to + * `COUNTERFACT_RESPONSE`. + * + * When a Response type carries an `examples` key it is a spec-generated + * response (either the initial no-body builder or a builder that still has + * content/headers to satisfy). Those always go through + * `GenericResponseBuilderInner`, which exposes `empty()` when `content` is + * `never`. + * + * When a Response type has no `examples` key it is a narrowed type produced + * by a method call (e.g. `.json()` sets the body and returns a type without + * `examples`). Those go through the existing collapse logic so that + * fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`. + */ +export type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = "examples" extends keyof Response + ? GenericResponseBuilderInner + : object extends OmitValueWhenNever> + ? COUNTERFACT_RESPONSE + : keyof OmitValueWhenNever> extends "headers" + ? COUNTERFACT_RESPONSE & { + header: HeaderFunction; + } + : GenericResponseBuilderInner; diff --git a/ordergroove/subscriptions/counterfact-types/http-status-code.ts b/ordergroove/subscriptions/counterfact-types/http-status-code.ts new file mode 100644 index 0000000..d809363 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/http-status-code.ts @@ -0,0 +1,62 @@ +/** + * A union of all standard HTTP status codes. + * Used to constrain the status code argument in response builder calls and + * generated route handler types. + */ +export type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; diff --git a/ordergroove/subscriptions/counterfact-types/if-has-key.ts b/ordergroove/subscriptions/counterfact-types/if-has-key.ts new file mode 100644 index 0000000..6608e83 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/if-has-key.ts @@ -0,0 +1,19 @@ +/** + * Conditional type that resolves to `Yes` when `SomeObject` has at least one + * key that contains any string from `Keys` as a substring, and `No` otherwise. + * Used to determine whether a shortcut method (e.g. `.json()`, `.html()`) + * should be present on the response builder for a given response type. + */ +export type IfHasKey< + SomeObject, + Keys extends readonly string[], + Yes, + No, +> = Keys extends [ + infer FirstKey extends string, + ...infer RestKeys extends string[], +] + ? Extract extends never + ? IfHasKey + : Yes + : No; diff --git a/ordergroove/subscriptions/counterfact-types/index.ts b/ordergroove/subscriptions/counterfact-types/index.ts new file mode 100644 index 0000000..91e246b --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/index.ts @@ -0,0 +1,21 @@ +export type { CookieOptions } from "./cookie-options.js"; +export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +export type { ExampleNames } from "./example-names.js"; +export type { + GenericResponseBuilder, + GenericResponseBuilderInner, +} from "./generic-response-builder.js"; +export type { HttpStatusCode } from "./http-status-code.js"; +export type { IfHasKey } from "./if-has-key.js"; +export type { MaybePromise } from "./maybe-promise.js"; +export type { MediaType } from "./media-type.js"; +export type { OmitAll } from "./omit-all.js"; +export type { OmitValueWhenNever } from "./omit-value-when-never.js"; +export type { OpenApiHeader } from "./open-api-header.js"; +export type { OpenApiOperation } from "./open-api-operation.js"; +export type { OpenApiParameters } from "./open-api-parameters.js"; +export type { OpenApiResponse } from "./open-api-response.js"; +export type { ResponseBuilder } from "./response-builder.js"; +export type { ResponseBuilderFactory } from "./response-builder-factory.js"; +export type { WideOperationArgument } from "./wide-operation-argument.js"; +export type { WideResponseBuilder } from "./wide-response-builder.js"; diff --git a/ordergroove/subscriptions/counterfact-types/maybe-promise.ts b/ordergroove/subscriptions/counterfact-types/maybe-promise.ts new file mode 100644 index 0000000..65a990e --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/maybe-promise.ts @@ -0,0 +1,6 @@ +/** + * A value that is either `T` directly or a `Promise`. + * Route handlers may return either synchronous values or promises, and + * Counterfact will await them transparently. + */ +export type MaybePromise = T | Promise; diff --git a/ordergroove/subscriptions/counterfact-types/media-type.ts b/ordergroove/subscriptions/counterfact-types/media-type.ts new file mode 100644 index 0000000..d3cc528 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/media-type.ts @@ -0,0 +1,6 @@ +/** + * Represents an IANA media type string in the format `type/subtype` + * (e.g. `"application/json"`, `"text/plain"`, `"image/png"`). + * Used to identify the content type of an HTTP request or response body. + */ +export type MediaType = `${string}/${string}`; diff --git a/ordergroove/subscriptions/counterfact-types/omit-all.ts b/ordergroove/subscriptions/counterfact-types/omit-all.ts new file mode 100644 index 0000000..0921eca --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/omit-all.ts @@ -0,0 +1,11 @@ +/** + * Removes all keys from `T` whose names contain any of the strings in `K` + * as a substring (prefix, suffix, or exact match). + * Used internally to narrow the set of available content-type methods on the + * response builder after one has already been called. + */ +export type OmitAll = { + [ + P in keyof T as P extends `${string}${K[number]}${string}` ? never : P + ]: T[P]; +}; diff --git a/ordergroove/subscriptions/counterfact-types/omit-value-when-never.ts b/ordergroove/subscriptions/counterfact-types/omit-value-when-never.ts new file mode 100644 index 0000000..e93f56b --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/omit-value-when-never.ts @@ -0,0 +1,11 @@ +/** + * Creates a new type from `Base` that omits any keys whose value type is + * `never`. This is used to strip unavailable builder methods (those that + * don't apply to the current response shape) from the fluent response builder. + */ +export type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; diff --git a/ordergroove/subscriptions/counterfact-types/open-api-content.ts b/ordergroove/subscriptions/counterfact-types/open-api-content.ts new file mode 100644 index 0000000..05d4bc8 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/open-api-content.ts @@ -0,0 +1,8 @@ +/** + * Represents a single content entry in an OpenAPI response object. + * The `schema` property holds the JSON Schema definition for the body of + * a response with this media type. + */ +export interface OpenApiContent { + schema: unknown; +} diff --git a/ordergroove/subscriptions/counterfact-types/open-api-header.ts b/ordergroove/subscriptions/counterfact-types/open-api-header.ts new file mode 100644 index 0000000..341f6a1 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/open-api-header.ts @@ -0,0 +1,4 @@ +export interface OpenApiHeader { + required?: boolean; + schema: { [key: string]: unknown }; +} diff --git a/ordergroove/subscriptions/counterfact-types/open-api-operation.ts b/ordergroove/subscriptions/counterfact-types/open-api-operation.ts new file mode 100644 index 0000000..b5cc9e3 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/open-api-operation.ts @@ -0,0 +1,36 @@ +import type { Example } from "./example.js"; +import type { OpenApiHeader } from "./open-api-header.js"; +import type { OpenApiParameters } from "./open-api-parameters.js"; + +/** + * Describes a single HTTP operation (e.g. `GET /pets`) as defined in an + * OpenAPI document. Used internally to derive the strongly-typed argument + * and response builder types for generated route handler functions. + */ +export interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + requestBody?: { + content?: { + [mediaType: string]: { + schema: { [key: string]: unknown }; + }; + }; + required?: boolean; + }; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: { [key: string]: unknown }; + }; + }; + examples?: { [key: string]: unknown }; + headers?: { + [name: string]: OpenApiHeader; + }; + schema?: { [key: string]: unknown }; + }; + }; +} diff --git a/ordergroove/subscriptions/counterfact-types/open-api-parameters.ts b/ordergroove/subscriptions/counterfact-types/open-api-parameters.ts new file mode 100644 index 0000000..9dad586 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/open-api-parameters.ts @@ -0,0 +1,26 @@ +/** + * Describes a single parameter (path, query, header, cookie, body, or + * formData) as defined in an OpenAPI document. Used internally to type the + * `path`, `query`, `headers`, and `body` properties of a route handler's + * argument object. + */ +export interface OpenApiParameters { + explode?: boolean; + in: + | "body" + | "cookie" + | "formData" + | "header" + | "path" + | "query" + | "querystring"; + name: string; + required?: boolean; + schema?: { + [key: string]: unknown; + properties?: Record; + type?: string; + }; + style?: string; + type?: "string" | "number" | "integer" | "boolean"; +} diff --git a/ordergroove/subscriptions/counterfact-types/open-api-response.ts b/ordergroove/subscriptions/counterfact-types/open-api-response.ts new file mode 100644 index 0000000..3d41c15 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/open-api-response.ts @@ -0,0 +1,22 @@ +import type { MediaType } from "./media-type.js"; +import type { OpenApiContent } from "./open-api-content.js"; + +/** + * Describes a single HTTP response as modelled in an OpenAPI document. + * Contains the allowed content types, optional named examples, and the + * required/optional response headers for that response. + */ +export interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + examples?: { [key: string]: unknown }; + headers: { [key: string]: { schema: unknown } }; + requiredHeaders: string; +} + +/** + * A map of HTTP status codes (or `"default"`) to their corresponding + * `OpenApiResponse` definitions for a given operation. + */ +export interface OpenApiResponses { + [key: string]: OpenApiResponse; +} diff --git a/ordergroove/subscriptions/counterfact-types/random-function.ts b/ordergroove/subscriptions/counterfact-types/random-function.ts new file mode 100644 index 0000000..332b5fe --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/random-function.ts @@ -0,0 +1,9 @@ +import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * The type of the `.random()` method on the response builder. + * When called, it randomly selects one of the available content-type examples + * and returns a completed `COUNTERFACT_RESPONSE`. + */ +export type RandomFunction = () => MaybePromise; diff --git a/ordergroove/subscriptions/counterfact-types/response-builder-factory.ts b/ordergroove/subscriptions/counterfact-types/response-builder-factory.ts new file mode 100644 index 0000000..15cd813 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/response-builder-factory.ts @@ -0,0 +1,16 @@ +import type { GenericResponseBuilder } from "./generic-response-builder.js"; +import type { OpenApiResponses } from "./open-api-response.js"; + +/** + * Maps each HTTP status code (or `"default"`) in an OpenAPI operation's + * response definitions to the corresponding `GenericResponseBuilder`. + * This is the type of the `response` property in a generated route handler's + * argument object, allowing handlers to call e.g. `response[200].json(body)`. + */ +export type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; diff --git a/ordergroove/subscriptions/counterfact-types/response-builder.ts b/ordergroove/subscriptions/counterfact-types/response-builder.ts new file mode 100644 index 0000000..b4bdd61 --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/response-builder.ts @@ -0,0 +1,36 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed, chainable response builder used in non-generated contexts + * (e.g. middleware or wide/catch-all route handlers) where the exact response + * shape is not statically known. For generated route handlers, prefer the + * strongly-typed `GenericResponseBuilder`. + */ +export interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + binary: (body: Uint8Array | string) => ResponseBuilder; + content?: { body: unknown; type: string }[]; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => ResponseBuilder; + empty: () => ResponseBuilder; + example: (name: string) => ResponseBuilder; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string | string[] }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => MaybePromise; + randomLegacy: () => MaybePromise; + status?: number; + stream: (iterable: AsyncIterable) => { + body: AsyncIterable; + contentType: string; + status?: number; + }; + text: (body: unknown) => ResponseBuilder; + xml: (body: unknown) => ResponseBuilder; +} diff --git a/ordergroove/subscriptions/counterfact-types/wide-operation-argument.ts b/ordergroove/subscriptions/counterfact-types/wide-operation-argument.ts new file mode 100644 index 0000000..ed5029f --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/wide-operation-argument.ts @@ -0,0 +1,17 @@ +import type { WideResponseBuilder } from "./wide-response-builder.js"; + +/** + * The loosely-typed argument object passed to wide (catch-all) route handlers. + * Unlike the generated operation argument types, all fields are typed as + * `unknown` or broad index signatures. Use this when writing handlers that + * should accept any request without compile-time schema enforcement. + */ +export interface WideOperationArgument { + body: unknown; + context: unknown; + headers: { [key: string]: string }; + path: { [key: string]: string }; + proxy: (url: string) => { proxyUrl: string }; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} diff --git a/ordergroove/subscriptions/counterfact-types/wide-response-builder.ts b/ordergroove/subscriptions/counterfact-types/wide-response-builder.ts new file mode 100644 index 0000000..a90c9aa --- /dev/null +++ b/ordergroove/subscriptions/counterfact-types/wide-response-builder.ts @@ -0,0 +1,27 @@ +import type { CookieOptions } from "./cookie-options.js"; +import type { MaybePromise } from "./maybe-promise.js"; + +/** + * A loosely-typed response builder used in wide (catch-all) route handlers + * where the response shape is not known at compile time. Unlike the generated + * `GenericResponseBuilder`, this interface accepts `unknown` for all body + * arguments and does not enforce content-type constraints. + */ +export interface WideResponseBuilder { + binary: (body: Uint8Array | string) => WideResponseBuilder; + empty: () => WideResponseBuilder; + example: (name: string) => WideResponseBuilder; + cookie: ( + name: string, + value: string, + options?: CookieOptions, + ) => WideResponseBuilder; + header: (body: unknown) => WideResponseBuilder; + html: (body: unknown) => WideResponseBuilder; + json: (body: unknown) => WideResponseBuilder; + match: (contentType: string, body: unknown) => WideResponseBuilder; + random: () => MaybePromise; + text: (body: unknown) => WideResponseBuilder; + xml: (body: unknown) => WideResponseBuilder; + stream: (body: AsyncIterable) => WideResponseBuilder; +} diff --git a/ordergroove/subscriptions/routes/_.context.ts b/ordergroove/subscriptions/routes/_.context.ts new file mode 100644 index 0000000..8be5265 --- /dev/null +++ b/ordergroove/subscriptions/routes/_.context.ts @@ -0,0 +1,131 @@ +import type { Context$ } from "../types/_.context.js"; +import type { Subscription } from "../types/components/schemas/Subscription.js"; + +export type SubscriptionFilters = { + customer?: string; + product?: string; + live?: boolean | "true" | "false"; + created_start?: string; + created_end?: string; +}; + +export type SeedSubscription = { + subscription: Subscription; + createdAt: string; +}; + +/** + * This is the default context for Counterfact. + * + * It defines the context object in the REPL + * and the $.context object in the code. + * + * Add properties and methods to suit your needs. + * + * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md + */ + +export class Context { + readonly apiKey = "ordergroove-local-api-key"; + + readonly #subscriptions = new Map(); + readonly #createdDates = new Map(); + + constructor($: Context$) { + void $; + } + + isAuthorized(apiKey: string | undefined): boolean { + return apiKey === this.apiKey; + } + + seedSubscriptions(entries: SeedSubscription[]): void { + this.#subscriptions.clear(); + this.#createdDates.clear(); + + for (const { subscription, createdAt } of entries) { + if (!subscription.public_id) continue; + + this.#subscriptions.set( + subscription.public_id, + structuredClone(subscription), + ); + this.#createdDates.set(subscription.public_id, createdAt); + } + } + + listSubscriptions(filters: SubscriptionFilters): Subscription[] { + const live = + filters.live === "true" + ? true + : filters.live === "false" + ? false + : filters.live; + + return [...this.#subscriptions.entries()] + .filter(([publicId, subscription]) => { + const createdAt = this.#createdDates.get(publicId); + return ( + (!filters.customer || + subscription.customer_id === filters.customer) && + (!filters.product || subscription.product_id === filters.product) && + (live === undefined || subscription.live === live) && + (!filters.created_start || + (createdAt !== undefined && createdAt >= filters.created_start)) && + (!filters.created_end || + (createdAt !== undefined && createdAt <= filters.created_end)) + ); + }) + .map(([, subscription]) => structuredClone(subscription)); + } + + getSubscription(publicId: string): Subscription | undefined { + const subscription = this.#subscriptions.get(publicId); + return subscription ? structuredClone(subscription) : undefined; + } + + replaceSubscription( + publicId: string, + input: Subscription, + ): Subscription | undefined { + const existing = this.#subscriptions.get(publicId); + if (!existing) return undefined; + + const subscription = { + ...structuredClone(input), + id: existing.id, + public_id: publicId, + }; + this.#subscriptions.set(publicId, subscription); + return structuredClone(subscription); + } + + cancelSubscription(publicId: string): Subscription | undefined { + return this.#setLive(publicId, false); + } + + reactivateSubscription(publicId: string): Subscription | undefined { + return this.#setLive(publicId, true); + } + + changeSubscriptionFrequency( + publicId: string, + frequency: Pick, + ): Subscription | undefined { + const existing = this.#subscriptions.get(publicId); + if (!existing) return undefined; + + const subscription = { ...existing, ...structuredClone(frequency) }; + this.#subscriptions.set(publicId, subscription); + return structuredClone(subscription); + } + + #setLive(publicId: string, live: boolean): Subscription | undefined { + const existing = this.#subscriptions.get(publicId); + if (!existing) return undefined; + + const subscription = { ...existing, live }; + this.#subscriptions.set(publicId, subscription); + return structuredClone(subscription); + } +} diff --git a/ordergroove/subscriptions/routes/_.middleware.ts b/ordergroove/subscriptions/routes/_.middleware.ts new file mode 100644 index 0000000..2e3a010 --- /dev/null +++ b/ordergroove/subscriptions/routes/_.middleware.ts @@ -0,0 +1,7 @@ +export const middleware = async ($: any, respondTo: any) => { + if (!$.context.isAuthorized($.auth.apiKey)) { + return $.response[401].json({ error: "Unauthorized" }); + } + + return respondTo($); +}; diff --git a/ordergroove/subscriptions/routes/subscriptions.ts b/ordergroove/subscriptions/routes/subscriptions.ts new file mode 100644 index 0000000..6344e72 --- /dev/null +++ b/ordergroove/subscriptions/routes/subscriptions.ts @@ -0,0 +1,9 @@ +import type { listSubscriptions } from "../types/paths/subscriptions.types.js"; + +export const GET: listSubscriptions = async ($) => { + return $.response[200].json({ + results: $.context.listSubscriptions($.query), + next: null, + previous: null, + } as never); +}; diff --git a/ordergroove/subscriptions/routes/subscriptions/{public_id}.ts b/ordergroove/subscriptions/routes/subscriptions/{public_id}.ts new file mode 100644 index 0000000..2ac09f8 --- /dev/null +++ b/ordergroove/subscriptions/routes/subscriptions/{public_id}.ts @@ -0,0 +1,20 @@ +import type { retrieveSubscription } from "../../types/paths/subscriptions/{public_id}.types.js"; +import type { updateSubscription } from "../../types/paths/subscriptions/{public_id}.types.js"; + +export const GET: retrieveSubscription = async ($) => { + const subscription = $.context.getSubscription($.path.public_id); + if (!subscription) { + return $.x.response[404].json({ error: "Subscription not found" }); + } + + return $.response[200].json(subscription); +}; + +export const PUT: updateSubscription = async ($) => { + const subscription = $.context.replaceSubscription($.path.public_id, $.body); + if (!subscription) { + return $.x.response[404].json({ error: "Subscription not found" }); + } + + return $.response[200].json(subscription); +}; diff --git a/ordergroove/subscriptions/routes/subscriptions/{public_id}/cancel.ts b/ordergroove/subscriptions/routes/subscriptions/{public_id}/cancel.ts new file mode 100644 index 0000000..48ff5e3 --- /dev/null +++ b/ordergroove/subscriptions/routes/subscriptions/{public_id}/cancel.ts @@ -0,0 +1,10 @@ +import type { cancelSubscription } from "../../../types/paths/subscriptions/{public_id}/cancel.types.js"; + +export const POST: cancelSubscription = async ($) => { + const subscription = $.context.cancelSubscription($.path.public_id); + if (!subscription) { + return $.x.response[404].json({ error: "Subscription not found" }); + } + + return $.response[200].json(subscription); +}; diff --git a/ordergroove/subscriptions/routes/subscriptions/{public_id}/change_frequency.ts b/ordergroove/subscriptions/routes/subscriptions/{public_id}/change_frequency.ts new file mode 100644 index 0000000..3564cdc --- /dev/null +++ b/ordergroove/subscriptions/routes/subscriptions/{public_id}/change_frequency.ts @@ -0,0 +1,13 @@ +import type { changeSubscriptionFrequency } from "../../../types/paths/subscriptions/{public_id}/change_frequency.types.js"; + +export const POST: changeSubscriptionFrequency = async ($) => { + const subscription = $.context.changeSubscriptionFrequency( + $.path.public_id, + $.body, + ); + if (!subscription) { + return $.x.response[404].json({ error: "Subscription not found" }); + } + + return $.response[200].json(subscription); +}; diff --git a/ordergroove/subscriptions/routes/subscriptions/{public_id}/reactivate.ts b/ordergroove/subscriptions/routes/subscriptions/{public_id}/reactivate.ts new file mode 100644 index 0000000..731441b --- /dev/null +++ b/ordergroove/subscriptions/routes/subscriptions/{public_id}/reactivate.ts @@ -0,0 +1,10 @@ +import type { reactivateSubscription } from "../../../types/paths/subscriptions/{public_id}/reactivate.types.js"; + +export const POST: reactivateSubscription = async ($) => { + const subscription = $.context.reactivateSubscription($.path.public_id); + if (!subscription) { + return $.x.response[404].json({ error: "Subscription not found" }); + } + + return $.response[200].json(subscription); +}; diff --git a/ordergroove/subscriptions/scenarios/index.ts b/ordergroove/subscriptions/scenarios/index.ts new file mode 100644 index 0000000..8f8ab53 --- /dev/null +++ b/ordergroove/subscriptions/scenarios/index.ts @@ -0,0 +1,89 @@ +import type { Scenario } from "../types/_.context.js"; +import type { Context } from "../routes/_.context.js"; + +/** + * Scenario scripts are plain TypeScript functions that receive the live REPL + * environment and can read or mutate server state. Run them from the REPL with: + * .scenario + */ + +/** + * Read or mutate the root context (same object routes see as $.context): + * $.context. = ; + * + * Load a context for a specific path: + * const petsCtx = $.loadContext("/pets"); + * + * Store a pre-configured route builder for later use in the REPL: + * $.routes.myRequest = $.route("/pets").method("get"); + */ + +/** + * startup() runs automatically when the server initializes, right before the + * REPL starts. Use it to seed dummy data so the server is ready to use + * immediately. It receives the same $ argument as all other scenario functions. + * + * Tip: delegate to other scenario functions and pass $ along so each function + * stays focused on a single concern. You can also pass additional arguments to + * configure them, e.g. addPets($, 20, "dog"). + * + * If you don't need a startup scenario, delete this function or leave it empty. + */ +export const startup: Scenario = ($) => { + const context = $.context as Context; + context.seedSubscriptions([ + { + subscription: { + id: "subscription-internal-001", + public_id: "subscription-001", + customer_id: "customer-001", + product_id: "product-001", + quantity: 1, + payment_id: "payment-001", + shipping_address_id: "address-001", + offer_id: "offer-profile-001", + every: 1, + every_period: "month", + live: true, + }, + createdAt: "2026-01-15", + }, + { + subscription: { + id: "subscription-internal-002", + public_id: "subscription-002", + customer_id: "customer-002", + product_id: "product-002", + quantity: 2, + payment_id: "payment-002", + shipping_address_id: "address-002", + offer_id: "offer-profile-002", + every: 2, + every_period: "week", + live: false, + }, + createdAt: "2026-02-20", + }, + ]); +}; + +/** + * An example scenario. To use it in the REPL, type: + * .scenario help + */ +export const help: Scenario = ($) => { + void $; + + console.log( + [ + "Scenarios are functions that populate the context object", + "and / or the REPL environment. They are intended to", + "populate your environment with specific data and", + "configurations for testing purposes.", + ].join("\n"), + ); + + console.log( + "\nScenarios (including this one) are defined in the ./scenarios directory.", + ); +}; diff --git a/ordergroove/subscriptions/test/context.test.ts b/ordergroove/subscriptions/test/context.test.ts new file mode 100644 index 0000000..963c86c --- /dev/null +++ b/ordergroove/subscriptions/test/context.test.ts @@ -0,0 +1,145 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "../routes/_.context.ts"; + +const createContext = () => new Context({} as never); + +const seed = (context: Context) => { + context.seedSubscriptions([ + { + subscription: { + id: "subscription-internal-001", + public_id: "subscription-001", + customer_id: "customer-001", + product_id: "product-001", + quantity: 1, + every: 1, + every_period: "month", + live: true, + }, + createdAt: "2026-01-15", + }, + { + subscription: { + id: "subscription-internal-002", + public_id: "subscription-002", + customer_id: "customer-002", + product_id: "product-002", + quantity: 2, + every: 2, + every_period: "week", + live: false, + }, + createdAt: "2026-02-20", + }, + ]); +}; + +test("authorizes only the configured API key", () => { + const context = createContext(); + + assert.equal(context.isAuthorized(context.apiKey), true); + assert.equal(context.isAuthorized("wrong"), false); + assert.equal(context.isAuthorized(undefined), false); +}); + +test("seeds, lists, and retrieves subscriptions without exposing mutable state", () => { + const context = createContext(); + seed(context); + + const listed = context.listSubscriptions({}); + assert.equal(listed.length, 2); + assert.equal(context.getSubscription("subscription-001")?.quantity, 1); + + listed[0]!.quantity = 99; + assert.equal(context.getSubscription("subscription-001")?.quantity, 1); +}); + +test("filters subscriptions by customer, product, live status, and inclusive creation dates", () => { + const context = createContext(); + seed(context); + + assert.deepEqual( + context + .listSubscriptions({ customer: "customer-001" }) + .map(({ public_id }) => public_id), + ["subscription-001"], + ); + assert.deepEqual( + context + .listSubscriptions({ product: "product-002", live: false }) + .map(({ public_id }) => public_id), + ["subscription-002"], + ); + assert.deepEqual( + context + .listSubscriptions({ + created_start: "2026-01-15", + created_end: "2026-01-15", + }) + .map(({ public_id }) => public_id), + ["subscription-001"], + ); + assert.deepEqual( + context + .listSubscriptions({ + created_start: "2026-01-16", + created_end: "2026-02-19", + }) + .map(({ public_id }) => public_id), + [], + ); +}); + +test("replacement preserves identifiers, removes omitted fields, and persists", () => { + const context = createContext(); + seed(context); + + const replaced = context.replaceSubscription("subscription-001", { + id: "ignored-id", + public_id: "ignored-public-id", + customer_id: "customer-002", + product_id: "product-002", + quantity: 4, + every: 3, + every_period: "month", + live: true, + }); + + assert.deepEqual(replaced, { + id: "subscription-internal-001", + public_id: "subscription-001", + customer_id: "customer-002", + product_id: "product-002", + quantity: 4, + every: 3, + every_period: "month", + live: true, + }); + assert.deepEqual(context.getSubscription("subscription-001"), replaced); + assert.equal(context.replaceSubscription("missing", {}), undefined); +}); + +test("cancels, reactivates, and changes subscription frequency persistently", () => { + const context = createContext(); + seed(context); + + assert.equal(context.cancelSubscription("subscription-001")?.live, false); + assert.equal(context.getSubscription("subscription-001")?.live, false); + assert.equal(context.reactivateSubscription("subscription-001")?.live, true); + + const changed = context.changeSubscriptionFrequency("subscription-001", { + every: 6, + every_period: "week", + }); + assert.equal(changed?.every, 6); + assert.equal(changed?.every_period, "week"); + assert.equal(context.getSubscription("subscription-001")?.every, 6); + + assert.equal(context.cancelSubscription("missing"), undefined); + assert.equal(context.reactivateSubscription("missing"), undefined); + assert.equal( + context.changeSubscriptionFrequency("missing", { every: 1 }), + undefined, + ); +}); diff --git a/ordergroove/subscriptions/test/routes.test.ts b/ordergroove/subscriptions/test/routes.test.ts new file mode 100644 index 0000000..2f19d0f --- /dev/null +++ b/ordergroove/subscriptions/test/routes.test.ts @@ -0,0 +1,230 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../../", import.meta.url)); +const openApiPath = fileURLToPath( + new URL("../../openapi/upstream/subscriptions.yml", import.meta.url), +); +const specifications = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +].map((group) => ({ + source: fileURLToPath( + new URL(`../../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/subscriptions/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("requires a valid API key", async () => { + const missing = await fetch(`http://127.0.0.1:${port}/subscriptions/`); + assert.equal(missing.status, 401); + assert.deepEqual(await missing.json(), { error: "Unauthorized" }); + + const invalid = await request("/subscriptions/", { + headers: { "x-api-key": "invalid" }, + }); + assert.equal(invalid.status, 401); +}); + +test("lists deterministic subscriptions and applies every documented filter", async () => { + const response = await request("/subscriptions/"); + assert.equal(response.status, 200); + const body = await response.json(); + assert.deepEqual( + body.results.map(({ public_id }: { public_id: string }) => public_id), + ["subscription-001", "subscription-002"], + ); + assert.equal(body.results[0].customer_id, "customer-001"); + assert.equal(body.results[0].product_id, "product-001"); + + for (const [query, expected] of [ + ["customer=customer-001", ["subscription-001"]], + ["product=product-002", ["subscription-002"]], + ["live=false", ["subscription-002"]], + ["created_start=2026-02-01", ["subscription-002"]], + ["created_end=2026-01-31", ["subscription-001"]], + ] as const) { + const filtered = await request(`/subscriptions/?${query}`); + assert.equal(filtered.status, 200); + assert.deepEqual( + (await filtered.json()).results.map( + ({ public_id }: { public_id: string }) => public_id, + ), + expected, + query, + ); + } +}); + +test("retrieves, replaces, and persists a subscription", async () => { + const retrieveResponse = await request("/subscriptions/subscription-001/"); + assert.equal(retrieveResponse.status, 200); + assert.equal((await retrieveResponse.json()).quantity, 1); + + const updateResponse = await request("/subscriptions/subscription-001/", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + id: "ignored-id", + public_id: "ignored-public-id", + customer_id: "customer-002", + product_id: "product-002", + quantity: 3, + every: 4, + every_period: "week", + live: true, + }), + }); + assert.equal(updateResponse.status, 200); + assert.deepEqual(await updateResponse.json(), { + id: "subscription-internal-001", + public_id: "subscription-001", + customer_id: "customer-002", + product_id: "product-002", + quantity: 3, + every: 4, + every_period: "week", + live: true, + }); + const persisted = await request("/subscriptions/subscription-001/"); + assert.equal(persisted.status, 200); + assert.equal((await persisted.json()).quantity, 3); +}); + +test("persists cancellation, reactivation, and frequency changes", async () => { + const cancel = await request("/subscriptions/subscription-001/cancel/", { + method: "POST", + }); + assert.equal(cancel.status, 200); + assert.equal((await cancel.json()).live, false); + const persistedCancel = await request("/subscriptions/subscription-001/"); + assert.equal((await persistedCancel.json()).live, false); + + const reactivate = await request( + "/subscriptions/subscription-001/reactivate/", + { method: "POST" }, + ); + assert.equal(reactivate.status, 200); + assert.equal((await reactivate.json()).live, true); + + const frequency = await request( + "/subscriptions/subscription-001/change_frequency/", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ every: 2, every_period: "year" }), + }, + ); + assert.equal(frequency.status, 200); + assert.equal((await frequency.json()).every, 2); + const persistedFrequency = await request("/subscriptions/subscription-001/"); + assert.equal((await persistedFrequency.json()).every_period, "year"); +}); + +test("returns 404 for every operation on unknown subscriptions", async () => { + const operations: Array<[string, RequestInit]> = [ + ["/subscriptions/not-found/", {}], + [ + "/subscriptions/not-found/", + { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ quantity: 1 }), + }, + ], + ["/subscriptions/not-found/cancel/", { method: "POST" }], + ["/subscriptions/not-found/reactivate/", { method: "POST" }], + [ + "/subscriptions/not-found/change_frequency/", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ every: 1, every_period: "month" }), + }, + ], + ]; + + for (const [pathname, init] of operations) { + const response = await request(pathname, init); + assert.equal(response.status, 404, pathname); + assert.deepEqual(await response.json(), { + error: "Subscription not found", + }); + } +}); diff --git a/ordergroove/subscriptions/types/#/components/responses/TooManyRequests.ts b/ordergroove/subscriptions/types/#/components/responses/TooManyRequests.ts new file mode 100644 index 0000000..89b6887 --- /dev/null +++ b/ordergroove/subscriptions/types/#/components/responses/TooManyRequests.ts @@ -0,0 +1,6 @@ +export type TooManyRequests = { + headers: never; + requiredHeaders: never; + content: never; + examples: {}; +}; diff --git a/ordergroove/subscriptions/types/#/components/responses/Unauthorized.ts b/ordergroove/subscriptions/types/#/components/responses/Unauthorized.ts new file mode 100644 index 0000000..d6753c8 --- /dev/null +++ b/ordergroove/subscriptions/types/#/components/responses/Unauthorized.ts @@ -0,0 +1,6 @@ +export type Unauthorized = { + headers: never; + requiredHeaders: never; + content: never; + examples: {}; +}; diff --git a/ordergroove/subscriptions/types/_.context.ts b/ordergroove/subscriptions/types/_.context.ts new file mode 100644 index 0000000..76042c6 --- /dev/null +++ b/ordergroove/subscriptions/types/_.context.ts @@ -0,0 +1,29 @@ +// This file is generated by Counterfact. Do not edit manually. +import type { Context } from "../routes/_.context"; + +interface LoadContextDefinitions { + /* code generator adds additional signatures here */ + loadContext(path: "/" | `/${string}`): Context; + loadContext(path: string): Record; +} + +export interface Scenario$ { + /** Root context, same as loadContext("/") */ + readonly context: Context; + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Named route builders stored in the REPL execution context */ + readonly routes: Record; + /** Create a new route builder for a given path */ + readonly route: (path: string) => unknown; +} + +/** A scenario function that receives the live REPL environment */ +export type Scenario = ($: Scenario$) => Promise | void; + +/** Interface for Context objects defined in _.context.ts files */ +export interface Context$ { + /** Load a context object for a specific path */ + readonly loadContext: LoadContextDefinitions["loadContext"]; + /** Load a JSON file relative to this file's path */ + readonly readJson: (relativePath: string) => Promise; +} diff --git a/ordergroove/subscriptions/types/components/schemas/Subscription.ts b/ordergroove/subscriptions/types/components/schemas/Subscription.ts new file mode 100644 index 0000000..059e6b6 --- /dev/null +++ b/ordergroove/subscriptions/types/components/schemas/Subscription.ts @@ -0,0 +1,13 @@ +export type Subscription = { + id?: string; + public_id?: string; + customer_id?: string; + product_id?: string; + quantity?: number; + payment_id?: string; + shipping_address_id?: string; + offer_id?: string; + every?: number; + every_period?: "day" | "week" | "month" | "year"; + live?: boolean; +}; diff --git a/ordergroove/subscriptions/types/components/schemas/SubscriptionList.ts b/ordergroove/subscriptions/types/components/schemas/SubscriptionList.ts new file mode 100644 index 0000000..e0446af --- /dev/null +++ b/ordergroove/subscriptions/types/components/schemas/SubscriptionList.ts @@ -0,0 +1,7 @@ +import type { Subscription } from "./Subscription.js"; + +export type SubscriptionList = { + results?: Array; + next?: string; + previous?: string; +}; diff --git a/ordergroove/subscriptions/types/paths/subscriptions.types.ts b/ordergroove/subscriptions/types/paths/subscriptions.types.ts new file mode 100644 index 0000000..2d428eb --- /dev/null +++ b/ordergroove/subscriptions/types/paths/subscriptions.types.ts @@ -0,0 +1,61 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts"; +import type { Context } from "../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts"; +import type { SubscriptionList } from "../components/schemas/SubscriptionList.js"; +import type { Unauthorized } from "../#/components/responses/Unauthorized.js"; +import type { TooManyRequests } from "../#/components/responses/TooManyRequests.js"; + +/** + * Lists subscriptions, filterable by customer, product, shipping address, live status, and created/updated date ranges. Listing across more than one customer requires the Bulk Operations permission. + */ +export type listSubscriptions = ( + $: OmitValueWhenNever<{ + query: listSubscriptions_Query; + querystring: never; + path: never; + headers: listSubscriptions_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: SubscriptionList; + }; + }; + examples: {}; + }; + 401: Unauthorized; + 429: TooManyRequests; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type listSubscriptions_Query = { + /** + * Filter by customer ID (Application scope only). + */ + customer?: string; + product?: string; + live?: boolean; + created_start?: string; + created_end?: string; +}; + +export type listSubscriptions_Headers = { "x-api-key": string }; diff --git a/ordergroove/subscriptions/types/paths/subscriptions/{public_id}.types.ts b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}.types.ts new file mode 100644 index 0000000..ea118f9 --- /dev/null +++ b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}.types.ts @@ -0,0 +1,87 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts"; +import type { Context } from "../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts"; +import type { Subscription } from "../../components/schemas/Subscription.js"; +import type { Unauthorized } from "../../#/components/responses/Unauthorized.js"; + +/** + * Retrieve a subscription + */ +export type retrieveSubscription = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: retrieveSubscription_Path; + headers: retrieveSubscription_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Subscription; + }; + }; + examples: {}; + }; + 401: Unauthorized; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +/** + * Update a subscription + */ +export type updateSubscription = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: updateSubscription_Path; + headers: updateSubscription_Headers; + cookie: never; + body: Subscription; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Subscription; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type retrieveSubscription_Path = { public_id: string }; + +export type retrieveSubscription_Headers = { "x-api-key": string }; + +export type updateSubscription_Path = { public_id: string }; + +export type updateSubscription_Headers = { "x-api-key": string }; diff --git a/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/cancel.types.ts b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/cancel.types.ts new file mode 100644 index 0000000..79c84fb --- /dev/null +++ b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/cancel.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.ts"; +import type { Context } from "../../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts"; +import type { Subscription } from "../../../components/schemas/Subscription.js"; + +/** + * Cancel a subscription + */ +export type cancelSubscription = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: cancelSubscription_Path; + headers: cancelSubscription_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Subscription; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type cancelSubscription_Path = { public_id: string }; + +export type cancelSubscription_Headers = { "x-api-key": string }; diff --git a/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/change_frequency.types.ts b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/change_frequency.types.ts new file mode 100644 index 0000000..d1bf8a9 --- /dev/null +++ b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/change_frequency.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.ts"; +import type { Context } from "../../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts"; +import type { Subscription } from "../../../components/schemas/Subscription.js"; + +/** + * Change subscription frequency + */ +export type changeSubscriptionFrequency = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: changeSubscriptionFrequency_Path; + headers: changeSubscriptionFrequency_Headers; + cookie: never; + body: { every?: number; every_period?: "day" | "week" | "month" | "year" }; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Subscription; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type changeSubscriptionFrequency_Path = { public_id: string }; + +export type changeSubscriptionFrequency_Headers = { "x-api-key": string }; diff --git a/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/reactivate.types.ts b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/reactivate.types.ts new file mode 100644 index 0000000..2379727 --- /dev/null +++ b/ordergroove/subscriptions/types/paths/subscriptions/{public_id}/reactivate.types.ts @@ -0,0 +1,48 @@ +// This code was automatically generated from an OpenAPI description. +// Do not edit this file. Edit the OpenAPI file instead. +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md + +import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; +import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; +import type { MaybePromise } from "../../../../counterfact-types/index.ts"; +import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.ts"; +import type { Context } from "../../../../routes/_.context.ts"; +import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts"; +import type { Subscription } from "../../../components/schemas/Subscription.js"; + +/** + * Reactivate a subscription + */ +export type reactivateSubscription = ( + $: OmitValueWhenNever<{ + query: never; + querystring: never; + path: reactivateSubscription_Path; + headers: reactivateSubscription_Headers; + cookie: never; + body: never; + context: Context; + response: ResponseBuilderFactory<{ + 200: { + headers: never; + requiredHeaders: never; + content: { + "application/json": { + schema: Subscription; + }; + }; + examples: {}; + }; + }>; + x: WideOperationArgument; + proxy: (url: string) => COUNTERFACT_RESPONSE; + auth: { apiKey: string }; + user: never; + delay: (milliseconds: number, maxMilliseconds?: number) => Promise; + version: never; + }>, +) => MaybePromise; + +export type reactivateSubscription_Path = { public_id: string }; + +export type reactivateSubscription_Headers = { "x-api-key": string }; diff --git a/ordergroove/test/integration.test.ts b/ordergroove/test/integration.test.ts new file mode 100644 index 0000000..905222d --- /dev/null +++ b/ordergroove/test/integration.test.ts @@ -0,0 +1,303 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import net from "node:net"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { counterfact } from "counterfact"; + +const basePath = fileURLToPath(new URL("../", import.meta.url)); +const groups = [ + "customers", + "items", + "offers", + "orders", + "products", + "subscriptions", +] as const; +const specifications = groups.map((group) => ({ + source: fileURLToPath( + new URL(`../openapi/upstream/${group}.yml`, import.meta.url), + ), + group, + prefix: "", +})); +const apiKey = "ordergroove-local-api-key"; + +type Resource = Record; + +let port: number; +let server: { stop(): Promise } | undefined; + +const request = (pathname: string, init: RequestInit = {}) => + fetch(`http://127.0.0.1:${port}${pathname}`, { + ...init, + headers: { "x-api-key": apiKey, ...init.headers }, + }); + +const results = async (pathname: string): Promise => { + const response = await request(pathname); + assert.equal(response.status, 200, pathname); + return ((await response.json()) as { results: Resource[] }).results; +}; + +const ids = (resources: Resource[], key = "id") => + new Set(resources.map((resource) => resource[key])); + +const getFreePort = async () => + new Promise((resolve, reject) => { + const temporaryServer = net.createServer(); + temporaryServer.listen(0, "127.0.0.1", () => { + const address = temporaryServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + reject(new Error("failed to determine a free port")); + } + temporaryServer.close(); + }); + temporaryServer.on("error", reject); + }); + +const waitForServer = async () => { + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + const response = await request("/customers/"); + if (response.ok) return; + } catch { + // The listener may not be ready yet. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Counterfact server did not start in time"); +}; + +test.before(async () => { + port = await getFreePort(); + const openApiPath = specifications[0].source; + const config = { + adminApiToken: "", + alwaysFakeOptionals: false, + basePath, + buildCache: false, + generate: { prune: false, routes: false, types: false }, + openApiPath, + port, + prefix: "", + proxyPaths: new Map([["", false]]), + proxyUrl: "", + startAdminApi: false, + startRepl: false, + startServer: true, + validateRequests: true, + validateResponses: true, + watch: { routes: false, types: false }, + }; + + const app = await counterfact(config, specifications); + server = await app.start(config); + await waitForServer(); +}); + +test.after(async () => { + await server?.stop(); +}); + +test("runs the combined simulator on the pinned Counterfact version", async () => { + const packageManifest = JSON.parse( + await readFile(new URL("../package.json", import.meta.url), "utf8"), + ) as { dependencies: { counterfact: string } }; + const installedManifest = JSON.parse( + await readFile( + new URL("../node_modules/counterfact/package.json", import.meta.url), + "utf8", + ), + ) as { version: string }; + + assert.equal(packageManifest.dependencies.counterfact, "2.14.2"); + assert.equal(installedManifest.version, "2.14.2"); +}); + +test("starts all six APIs with their seeded scenarios at canonical paths", async () => { + const canonicalCollections = [ + "/customers/", + "/items/", + "/offer_profiles/", + "/otd/", + "/entitlements/", + "/orders/", + "/products/", + "/subscriptions/", + ]; + + for (const pathname of canonicalCollections) { + const response = await request(pathname); + assert.equal(response.status, 200, pathname); + assert.ok( + ((await response.json()) as { results: Resource[] }).results.length > 0, + `${pathname} should contain deterministic startup data`, + ); + } + + for (const pathname of [ + "/customers/customers/", + "/items/items/", + "/offers/offer_profiles/", + "/orders/orders/", + "/products/products/", + "/subscriptions/subscriptions/", + ]) { + const response = await request(pathname); + assert.equal(response.status, 404, pathname); + } +}); + +test("requires an API key for every API group", async () => { + for (const pathname of [ + "/customers/", + "/items/", + "/offer_profiles/", + "/orders/", + "/products/", + "/subscriptions/", + ]) { + const response = await fetch(`http://127.0.0.1:${port}${pathname}`); + assert.equal(response.status, 401, pathname); + assert.deepEqual(await response.json(), { error: "Unauthorized" }); + } +}); + +test("keeps seeded customer commerce chains coherent across APIs", async () => { + const [ + customers, + products, + offers, + entitlements, + subscriptions, + orders, + items, + ] = await Promise.all([ + results("/customers/"), + results("/products/"), + results("/offer_profiles/"), + results("/entitlements/"), + results("/subscriptions/"), + results("/orders/"), + results("/items/"), + ]); + + const customerIds = ids(customers, "public_id"); + const productIds = ids(products); + const offerIds = ids(offers); + const subscriptionIds = ids(subscriptions, "public_id"); + const orderIds = ids(orders, "public_id"); + + for (const entitlement of entitlements) { + assert.ok(customerIds.has(entitlement.customer_id)); + } + for (const subscription of subscriptions) { + assert.ok(customerIds.has(subscription.customer_id)); + assert.ok(productIds.has(subscription.product_id)); + assert.ok(offerIds.has(subscription.offer_id)); + } + for (const order of orders) { + assert.ok(customerIds.has(order.customer_id)); + } + for (const item of items) { + assert.ok(orderIds.has(item.order_id)); + assert.ok(subscriptionIds.has(item.subscription_id)); + assert.ok(productIds.has(item.product_id)); + assert.ok(offerIds.has(item.offer_id)); + } + + const customerOneEntitlements = await results( + "/entitlements/?customer=customer-001", + ); + const customerOneSubscriptions = await results( + "/subscriptions/?customer=customer-001", + ); + const customerOneOrders = await results("/orders/?customer=customer-001"); + const subscriptionOneItems = await results( + "/items/?subscription=subscription-001", + ); + + assert.ok( + customerOneEntitlements.every( + (entitlement) => entitlement.customer_id === "customer-001", + ), + ); + assert.deepEqual( + customerOneSubscriptions.map((subscription) => subscription.public_id), + ["subscription-001"], + ); + assert.deepEqual( + customerOneOrders.map((order) => order.public_id), + ["order-001"], + ); + assert.deepEqual( + subscriptionOneItems.map((item) => item.public_id), + ["item-001"], + ); +}); + +test("persists representative cross-resource changes on the combined server", async () => { + const discountResponse = await request("/otd/", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + customer_id: "customer-001", + amount: "3.00", + type: "fixed", + }), + }); + assert.equal(discountResponse.status, 201); + const discount = (await discountResponse.json()) as Resource; + assert.equal(discount.customer_id, "customer-001"); + + const cancelResponse = await request( + "/subscriptions/subscription-001/cancel/", + { method: "POST" }, + ); + assert.equal(cancelResponse.status, 200); + assert.equal(((await cancelResponse.json()) as Resource).live, false); + + const sendNowResponse = await request("/orders/order-001/send_now/", { + method: "POST", + }); + assert.equal(sendNowResponse.status, 200); + assert.equal(((await sendNowResponse.json()) as Resource).status, "pending"); + + const createItemResponse = await request("/items/", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + order_id: "order-001", + subscription_id: "subscription-001", + product_id: "product-001", + quantity: 1, + price: "19.99", + total_price: "19.99", + offer_id: "offer-profile-001", + one_time: true, + }), + }); + assert.equal(createItemResponse.status, 201); + const createdItem = (await createItemResponse.json()) as Resource; + + const [discounts, persistedSubscription, persistedOrder, persistedItem] = + await Promise.all([ + results("/otd/"), + request("/subscriptions/subscription-001/"), + request("/orders/order-001/"), + request(`/items/${createdItem.public_id as string}/`), + ]); + assert.ok(discounts.some(({ id }) => id === discount.id)); + assert.equal(persistedSubscription.status, 200); + assert.equal(((await persistedSubscription.json()) as Resource).live, false); + assert.equal(persistedOrder.status, 200); + assert.equal(((await persistedOrder.json()) as Resource).status, "pending"); + assert.equal(persistedItem.status, 200); + assert.equal( + ((await persistedItem.json()) as Resource).subscription_id, + "subscription-001", + ); +}); diff --git a/ordergroove/tsconfig.json b/ordergroove/tsconfig.json new file mode 100644 index 0000000..993a3ba --- /dev/null +++ b/ordergroove/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "types": ["node"], + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["**/*.ts"] +}