@fluixi/api 0.0.0-stage → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -2
  3. package/dist/client.cjs +103 -0
  4. package/dist/client.d.ts +95 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +87 -0
  7. package/dist/client.mjs +82 -0
  8. package/dist/errors.cjs +63 -0
  9. package/dist/errors.d.ts +47 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +55 -0
  12. package/dist/errors.mjs +42 -0
  13. package/dist/index.cjs +667 -0
  14. package/dist/index.d.ts +34 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +26 -0
  17. package/dist/index.mjs +5442 -0
  18. package/dist/input.cjs +91 -0
  19. package/dist/input.d.ts +15 -0
  20. package/dist/input.d.ts.map +1 -0
  21. package/dist/input.js +73 -0
  22. package/dist/input.mjs +68 -0
  23. package/dist/middleware.cjs +41 -0
  24. package/dist/middleware.d.ts +9 -0
  25. package/dist/middleware.d.ts.map +1 -0
  26. package/dist/middleware.js +19 -0
  27. package/dist/middleware.mjs +20 -0
  28. package/dist/openapi.cjs +282 -0
  29. package/dist/openapi.d.ts +60 -0
  30. package/dist/openapi.d.ts.map +1 -0
  31. package/dist/openapi.js +312 -0
  32. package/dist/openapi.mjs +259 -0
  33. package/dist/rate-limit.cjs +158 -0
  34. package/dist/rate-limit.d.ts +69 -0
  35. package/dist/rate-limit.d.ts.map +1 -0
  36. package/dist/rate-limit.js +121 -0
  37. package/dist/rate-limit.mjs +4935 -0
  38. package/dist/route.cjs +192 -0
  39. package/dist/route.d.ts +97 -0
  40. package/dist/route.d.ts.map +1 -0
  41. package/dist/route.js +98 -0
  42. package/dist/route.mjs +169 -0
  43. package/dist/security.cjs +222 -0
  44. package/dist/security.d.ts +64 -0
  45. package/dist/security.d.ts.map +1 -0
  46. package/dist/security.js +135 -0
  47. package/dist/security.mjs +4997 -0
  48. package/dist/standard-schema.cjs +18 -0
  49. package/dist/standard-schema.d.ts +39 -0
  50. package/dist/standard-schema.d.ts.map +1 -0
  51. package/dist/standard-schema.js +6 -0
  52. package/dist/standard-schema.mjs +0 -0
  53. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  54. package/package.json +54 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ibrahima Touré
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ # @fluixi/api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ API route contracts for Fluixi: declare what a route accepts, returns and requires, and get
4
+ validation, a consistent error model, an OpenAPI document and a typed client from the same
5
+ declaration.
6
+
7
+ ```ts
8
+ // src/api/v1/projects/index.ts
9
+ import { route } from '@fluixi/api';
10
+ import { z } from 'zod';
11
+
12
+ export const POST = route({
13
+ body: z.object({ name: z.string().min(1) }),
14
+ response: Project,
15
+ status: 201,
16
+ handler: ({ body }) => createProject(body.name),
17
+ });
18
+ ```
19
+
20
+ - **Input**: `params`, `query` and `body`, with any Standard Schema validator (Zod, Valibot,
21
+ ArkType). Every problem is reported at once, as a 400 `validation_failed`.
22
+ - **Output**: the `response` schema is applied on the way out, so fields it does not name never
23
+ reach the caller.
24
+ - **Errors**: one shape, `{ error: { code, message, details? } }`. `ApiError` and `notFound`,
25
+ `conflict` and the others for refusals; anything unexpected is a 500 with a generic message.
26
+ - **Guards**: `route({ guard })` runs before the input is read and types `auth` in the handler.
27
+ `signedIn()` and `member()` come from `@fluixi/auth/server`.
28
+ - **Revalidation**: `route({ revalidates: ['orgs'] })` names the cached data a success changes;
29
+ the client refreshes the pages showing it.
30
+ - **OpenAPI**: `openApiRoute(() => apiRoutes, info)` serves a 3.1 document generated from the
31
+ contracts; `describedBy` includes a library's own document.
32
+ - **Typed client**: `createClient<ApiRoutes>()` from `@fluixi/api/client` knows each route's
33
+ params, query, body and response.
34
+ - **Protection**: `apiSecurity({ origins, rateLimits })` for headers, CORS, the cross-site check
35
+ and rate limits.
36
+
37
+ Documentation: [API contracts](https://docs.fluixi.com/v1/en/api-contracts) and
38
+ [OpenAPI](https://docs.fluixi.com/v1/en/openapi).
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ ApiClientError: () => ApiClientError,
24
+ createClient: () => createClient
25
+ });
26
+ module.exports = __toCommonJS(client_exports);
27
+ var ROUTER_REVALIDATE = /* @__PURE__ */ Symbol.for("fluixi.revalidate");
28
+ function defaultRevalidate(names) {
29
+ if (typeof window === "undefined") return;
30
+ const revalidate = globalThis[ROUTER_REVALIDATE];
31
+ revalidate?.(names);
32
+ }
33
+ var ApiClientError = class extends Error {
34
+ constructor(status, code, message, details) {
35
+ super(message);
36
+ this.name = "ApiClientError";
37
+ this.status = status;
38
+ this.code = code;
39
+ this.details = details;
40
+ }
41
+ };
42
+ function createClient(options = {}) {
43
+ const { baseUrl = "", credentials = "same-origin" } = options;
44
+ async function call(method, path, input = {}) {
45
+ const fetcher = options.fetch ?? globalThis.fetch;
46
+ const headers = new Headers(typeof options.headers === "function" ? await options.headers() : options.headers);
47
+ for (const [name, value] of new Headers(input.headers)) headers.set(name, value);
48
+ headers.set("accept", "application/json");
49
+ let body;
50
+ if (input.body !== void 0) {
51
+ body = JSON.stringify(input.body);
52
+ headers.set("content-type", "application/json");
53
+ }
54
+ const response = await fetcher(baseUrl + fill(path, input.params) + search(input.query), {
55
+ method,
56
+ headers,
57
+ body,
58
+ credentials,
59
+ signal: input.signal
60
+ });
61
+ const json = (response.headers.get("content-type") ?? "").includes("json");
62
+ if (!response.ok) {
63
+ const error = json ? (await response.json().catch(() => null))?.error : void 0;
64
+ throw new ApiClientError(
65
+ response.status,
66
+ typeof error?.code === "string" ? error.code : "http_error",
67
+ typeof error?.message === "string" ? error.message : response.statusText || `HTTP ${response.status}`,
68
+ error?.details
69
+ );
70
+ }
71
+ const stale = response.headers.get("x-fx-revalidate");
72
+ if (stale && options.onRevalidate !== false) {
73
+ const names = stale.split(",").map((name) => name.trim()).filter(Boolean);
74
+ if (names.length) (options.onRevalidate ?? defaultRevalidate)(names);
75
+ }
76
+ if (response.status === 204 || response.status === 205) return void 0;
77
+ return json ? response.json() : response.text();
78
+ }
79
+ const bind = (method) => (path, input) => call(method, path, input);
80
+ return {
81
+ get: bind("GET"),
82
+ post: bind("POST"),
83
+ put: bind("PUT"),
84
+ patch: bind("PATCH"),
85
+ delete: bind("DELETE")
86
+ };
87
+ }
88
+ function fill(path, params = {}) {
89
+ return path.replace(/\{([^}]+)\}/g, (_, name) => {
90
+ const value = params[name];
91
+ if (value === void 0 || value === null) throw new TypeError(`Missing path parameter "${name}" for ${path}`);
92
+ return String(value).split("/").map(encodeURIComponent).join("/");
93
+ });
94
+ }
95
+ function search(query = {}) {
96
+ const params = new URLSearchParams();
97
+ for (const [key, value] of Object.entries(query)) {
98
+ if (value === void 0 || value === null) continue;
99
+ for (const one of Array.isArray(value) ? value : [value]) params.append(key, String(one));
100
+ }
101
+ const text = params.toString();
102
+ return text ? `?${text}` : "";
103
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A typed client for a Fluixi API, inferred from its route contracts.
3
+ *
4
+ * The API app's `src/api.gen.ts` (written by `@fluixi/start`) maps each path template to its
5
+ * route module; a client typed with it knows, per method, the params, query and body a route
6
+ * accepts and what it returns. Nothing from the server is bundled: the map is types only.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createClient } from '@fluixi/api/client';
11
+ * import type { ApiRoutes } from '@acme/api/routes';
12
+ *
13
+ * const api = createClient<ApiRoutes>();
14
+ * const project = await api.get('/api/v1/projects/{id}', { params: { id } });
15
+ * ```
16
+ */
17
+ import type { RouteHandler } from './route.js';
18
+ import type { StandardSchemaV1 as Schema } from './standard-schema.js';
19
+ type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
20
+ /**
21
+ * What `api.gen.ts` declares: path template to route module. `object` rather than an index
22
+ * signature, which the generated interface would not satisfy.
23
+ */
24
+ export type ApiRouteMap = object;
25
+ type ParamNames<P extends string> = P extends `${string}{${infer Name}}${infer Rest}` ? Name | ParamNames<Rest> : never;
26
+ type Parts<H> = H extends RouteHandler<infer P, infer Q, infer B, infer R, any> ? {
27
+ contracted: true;
28
+ params: P;
29
+ query: Q;
30
+ body: B;
31
+ response: R;
32
+ } : {
33
+ contracted: false;
34
+ params: undefined;
35
+ query: undefined;
36
+ body: undefined;
37
+ response: undefined;
38
+ };
39
+ type InputOf<S, Fallback> = S extends Schema ? Schema.InferInput<S> : Fallback;
40
+ /** Optional when every field inside may be left out. */
41
+ type Field<K extends string, T> = [T] extends [undefined] ? {
42
+ [_ in K]?: undefined;
43
+ } : {} extends T ? {
44
+ [_ in K]?: T;
45
+ } : {
46
+ [_ in K]: T;
47
+ };
48
+ type ParamsInput<Path extends string, S> = [ParamNames<Path>] extends [never] ? undefined : InputOf<S, Record<ParamNames<Path>, string | number>>;
49
+ type Flatten<T> = {
50
+ [K in keyof T]: T[K];
51
+ } & {};
52
+ /** The second argument of a call: what the route takes, plus per-call request options. */
53
+ export type CallOptions<Path extends string, H> = Flatten<Field<'params', ParamsInput<Path, Parts<H>['params']>> & Field<'query', InputOf<Parts<H>['query'], Parts<H>['contracted'] extends true ? undefined : Record<string, unknown>>> & Field<'body', InputOf<Parts<H>['body'], Parts<H>['contracted'] extends true ? undefined : unknown>> & {
54
+ headers?: HeadersInit;
55
+ signal?: AbortSignal;
56
+ }>;
57
+ /** What a call resolves to: the response schema's output, `unknown` for a route without one. */
58
+ export type CallResult<H> = Parts<H>['response'] extends Schema ? Schema.InferOutput<Parts<H>['response']> : unknown;
59
+ type PathsWith<Routes extends ApiRouteMap, M extends Method> = {
60
+ [P in keyof Routes & string]: M extends keyof Routes[P] ? P : never;
61
+ }[keyof Routes & string];
62
+ type Handler<Routes extends ApiRouteMap, P extends keyof Routes, M extends Method> = Routes[P][M & keyof Routes[P]];
63
+ type Call<Routes extends ApiRouteMap, M extends Method> = <P extends PathsWith<Routes, M>>(path: P, ...options: {} extends CallOptions<P, Handler<Routes, P, M>> ? [options?: CallOptions<P, Handler<Routes, P, M>>] : [options: CallOptions<P, Handler<Routes, P, M>>]) => Promise<CallResult<Handler<Routes, P, M>>>;
64
+ export interface ApiClient<Routes extends ApiRouteMap> {
65
+ get: Call<Routes, 'GET'>;
66
+ post: Call<Routes, 'POST'>;
67
+ put: Call<Routes, 'PUT'>;
68
+ patch: Call<Routes, 'PATCH'>;
69
+ delete: Call<Routes, 'DELETE'>;
70
+ }
71
+ export interface ClientOptions {
72
+ /** Prepended to every path. Empty by default: the page's own origin, where the web app forwards `/api`. */
73
+ baseUrl?: string;
74
+ /** Sent with every call. A function runs per call: during SSR, to forward the visitor's cookie. */
75
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
76
+ /** `same-origin` by default; `include` for a widget calling the API from another site. */
77
+ credentials?: RequestCredentials;
78
+ /** `globalThis.fetch` by default. */
79
+ fetch?: typeof fetch;
80
+ /**
81
+ * Called with the cached data names a successful response says it changed (`x-fx-revalidate`).
82
+ * By default, in a browser, the router revalidates them. `false` ignores the header.
83
+ */
84
+ onRevalidate?: ((names: string[]) => void) | false;
85
+ }
86
+ /** A refusal or failure, as the API's error model reports it. */
87
+ export declare class ApiClientError extends Error {
88
+ readonly status: number;
89
+ readonly code: string;
90
+ readonly details: unknown;
91
+ constructor(status: number, code: string, message: string, details?: unknown);
92
+ }
93
+ export declare function createClient<Routes extends ApiRouteMap>(options?: ClientOptions): ApiClient<Routes>;
94
+ export {};
95
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,87 @@
1
+ // The router registers itself here (`@fluixi/core`), so the client needs no import of it.
2
+ const ROUTER_REVALIDATE = Symbol.for('fluixi.revalidate');
3
+ function defaultRevalidate(names) {
4
+ // A server render has no routes on screen to reload.
5
+ if (typeof window === 'undefined')
6
+ return;
7
+ const revalidate = globalThis[ROUTER_REVALIDATE];
8
+ revalidate?.(names);
9
+ }
10
+ /** A refusal or failure, as the API's error model reports it. */
11
+ export class ApiClientError extends Error {
12
+ status;
13
+ code;
14
+ details;
15
+ constructor(status, code, message, details) {
16
+ super(message);
17
+ this.name = 'ApiClientError';
18
+ this.status = status;
19
+ this.code = code;
20
+ this.details = details;
21
+ }
22
+ }
23
+ export function createClient(options = {}) {
24
+ const { baseUrl = '', credentials = 'same-origin' } = options;
25
+ async function call(method, path, input = {}) {
26
+ const fetcher = options.fetch ?? globalThis.fetch;
27
+ const headers = new Headers(typeof options.headers === 'function' ? await options.headers() : options.headers);
28
+ for (const [name, value] of new Headers(input.headers))
29
+ headers.set(name, value);
30
+ headers.set('accept', 'application/json');
31
+ let body;
32
+ if (input.body !== undefined) {
33
+ body = JSON.stringify(input.body);
34
+ headers.set('content-type', 'application/json');
35
+ }
36
+ const response = await fetcher(baseUrl + fill(path, input.params) + search(input.query), {
37
+ method,
38
+ headers,
39
+ body,
40
+ credentials,
41
+ signal: input.signal,
42
+ });
43
+ const json = (response.headers.get('content-type') ?? '').includes('json');
44
+ if (!response.ok) {
45
+ const error = json ? (await response.json().catch(() => null))?.error : undefined;
46
+ throw new ApiClientError(response.status, typeof error?.code === 'string' ? error.code : 'http_error', typeof error?.message === 'string' ? error.message : response.statusText || `HTTP ${response.status}`, error?.details);
47
+ }
48
+ const stale = response.headers.get('x-fx-revalidate');
49
+ if (stale && options.onRevalidate !== false) {
50
+ const names = stale.split(',').map((name) => name.trim()).filter(Boolean);
51
+ if (names.length)
52
+ (options.onRevalidate ?? defaultRevalidate)(names);
53
+ }
54
+ if (response.status === 204 || response.status === 205)
55
+ return undefined;
56
+ return json ? response.json() : response.text();
57
+ }
58
+ const bind = (method) => (path, input) => call(method, path, input);
59
+ return {
60
+ get: bind('GET'),
61
+ post: bind('POST'),
62
+ put: bind('PUT'),
63
+ patch: bind('PATCH'),
64
+ delete: bind('DELETE'),
65
+ };
66
+ }
67
+ /** `{id}` from params, encoded; a catch-all keeps its slashes. */
68
+ function fill(path, params = {}) {
69
+ return path.replace(/\{([^}]+)\}/g, (_, name) => {
70
+ const value = params[name];
71
+ if (value === undefined || value === null)
72
+ throw new TypeError(`Missing path parameter "${name}" for ${path}`);
73
+ return String(value).split('/').map(encodeURIComponent).join('/');
74
+ });
75
+ }
76
+ /** Repeated keys for arrays, the way the server reads them; undefined left out. */
77
+ function search(query = {}) {
78
+ const params = new URLSearchParams();
79
+ for (const [key, value] of Object.entries(query)) {
80
+ if (value === undefined || value === null)
81
+ continue;
82
+ for (const one of Array.isArray(value) ? value : [value])
83
+ params.append(key, String(one));
84
+ }
85
+ const text = params.toString();
86
+ return text ? `?${text}` : '';
87
+ }
@@ -0,0 +1,82 @@
1
+ // src/client.ts
2
+ var ROUTER_REVALIDATE = /* @__PURE__ */ Symbol.for("fluixi.revalidate");
3
+ function defaultRevalidate(names) {
4
+ if (typeof window === "undefined") return;
5
+ const revalidate = globalThis[ROUTER_REVALIDATE];
6
+ revalidate?.(names);
7
+ }
8
+ var ApiClientError = class extends Error {
9
+ constructor(status, code, message, details) {
10
+ super(message);
11
+ this.name = "ApiClientError";
12
+ this.status = status;
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ };
17
+ function createClient(options = {}) {
18
+ const { baseUrl = "", credentials = "same-origin" } = options;
19
+ async function call(method, path, input = {}) {
20
+ const fetcher = options.fetch ?? globalThis.fetch;
21
+ const headers = new Headers(typeof options.headers === "function" ? await options.headers() : options.headers);
22
+ for (const [name, value] of new Headers(input.headers)) headers.set(name, value);
23
+ headers.set("accept", "application/json");
24
+ let body;
25
+ if (input.body !== void 0) {
26
+ body = JSON.stringify(input.body);
27
+ headers.set("content-type", "application/json");
28
+ }
29
+ const response = await fetcher(baseUrl + fill(path, input.params) + search(input.query), {
30
+ method,
31
+ headers,
32
+ body,
33
+ credentials,
34
+ signal: input.signal
35
+ });
36
+ const json = (response.headers.get("content-type") ?? "").includes("json");
37
+ if (!response.ok) {
38
+ const error = json ? (await response.json().catch(() => null))?.error : void 0;
39
+ throw new ApiClientError(
40
+ response.status,
41
+ typeof error?.code === "string" ? error.code : "http_error",
42
+ typeof error?.message === "string" ? error.message : response.statusText || `HTTP ${response.status}`,
43
+ error?.details
44
+ );
45
+ }
46
+ const stale = response.headers.get("x-fx-revalidate");
47
+ if (stale && options.onRevalidate !== false) {
48
+ const names = stale.split(",").map((name) => name.trim()).filter(Boolean);
49
+ if (names.length) (options.onRevalidate ?? defaultRevalidate)(names);
50
+ }
51
+ if (response.status === 204 || response.status === 205) return void 0;
52
+ return json ? response.json() : response.text();
53
+ }
54
+ const bind = (method) => (path, input) => call(method, path, input);
55
+ return {
56
+ get: bind("GET"),
57
+ post: bind("POST"),
58
+ put: bind("PUT"),
59
+ patch: bind("PATCH"),
60
+ delete: bind("DELETE")
61
+ };
62
+ }
63
+ function fill(path, params = {}) {
64
+ return path.replace(/\{([^}]+)\}/g, (_, name) => {
65
+ const value = params[name];
66
+ if (value === void 0 || value === null) throw new TypeError(`Missing path parameter "${name}" for ${path}`);
67
+ return String(value).split("/").map(encodeURIComponent).join("/");
68
+ });
69
+ }
70
+ function search(query = {}) {
71
+ const params = new URLSearchParams();
72
+ for (const [key, value] of Object.entries(query)) {
73
+ if (value === void 0 || value === null) continue;
74
+ for (const one of Array.isArray(value) ? value : [value]) params.append(key, String(one));
75
+ }
76
+ const text = params.toString();
77
+ return text ? `?${text}` : "";
78
+ }
79
+ export {
80
+ ApiClientError,
81
+ createClient
82
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/errors.ts
21
+ var errors_exports = {};
22
+ __export(errors_exports, {
23
+ ApiError: () => ApiError,
24
+ badRequest: () => badRequest,
25
+ conflict: () => conflict,
26
+ errorResponse: () => errorResponse,
27
+ forbidden: () => forbidden,
28
+ notFound: () => notFound,
29
+ toApiError: () => toApiError,
30
+ unauthorized: () => unauthorized
31
+ });
32
+ module.exports = __toCommonJS(errors_exports);
33
+ var ApiError = class extends Error {
34
+ constructor(status, code, message, details) {
35
+ super(message);
36
+ this.name = "ApiError";
37
+ this.status = status;
38
+ this.code = code;
39
+ this.details = details;
40
+ }
41
+ };
42
+ var badRequest = (message, details) => new ApiError(400, "bad_request", message, details);
43
+ var unauthorized = (message = "Sign in to continue.") => new ApiError(401, "unauthorized", message);
44
+ var forbidden = (message = "Not allowed.") => new ApiError(403, "forbidden", message);
45
+ var notFound = (message = "Not found.") => new ApiError(404, "not_found", message);
46
+ var conflict = (message, details) => new ApiError(409, "conflict", message, details);
47
+ function toApiError(error) {
48
+ if (error instanceof ApiError) return { error, internal: false };
49
+ const { status, code, message } = error ?? {};
50
+ if (status === 401 || status === 403) {
51
+ if (typeof code === "string" && typeof message === "string") return { error: new ApiError(status, code, message), internal: false };
52
+ return { error: status === 401 ? unauthorized() : forbidden(), internal: false };
53
+ }
54
+ return { error: new ApiError(500, "internal", "Something went wrong on our side."), internal: true };
55
+ }
56
+ function errorResponse(error) {
57
+ const body = { error: { code: error.code, message: error.message } };
58
+ if (error.details !== void 0) body.error.details = error.details;
59
+ return new Response(JSON.stringify(body), {
60
+ status: error.status,
61
+ headers: { "content-type": "application/json; charset=utf-8" }
62
+ });
63
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * One error model for every contracted route.
3
+ *
4
+ * A failure is always `{ error: { code, message, details? } }` with a matching HTTP status. The
5
+ * code is for programs, the message for people. Nothing from the server's internals reaches the
6
+ * response: an error that is not an `ApiError` becomes a 500 with a generic message, and the
7
+ * original is handed to `onError` for logging.
8
+ */
9
+ /** A failure a handler means to report, with the status and code the caller gets. */
10
+ export declare class ApiError extends Error {
11
+ readonly status: number;
12
+ readonly code: string;
13
+ readonly details: unknown;
14
+ constructor(status: number, code: string, message: string, details?: unknown);
15
+ }
16
+ /** The body of every error response. */
17
+ export interface ApiErrorBody {
18
+ error: {
19
+ code: string;
20
+ message: string;
21
+ details?: unknown;
22
+ };
23
+ }
24
+ /** One problem with the input, as reported in `details` of a `validation_failed` error. */
25
+ export interface ValidationIssue {
26
+ /** Where: `body.items.0.name`, `query.page`, `params.id`. */
27
+ path: string;
28
+ message: string;
29
+ }
30
+ export declare const badRequest: (message: string, details?: unknown) => ApiError;
31
+ export declare const unauthorized: (message?: string) => ApiError;
32
+ export declare const forbidden: (message?: string) => ApiError;
33
+ export declare const notFound: (message?: string) => ApiError;
34
+ export declare const conflict: (message: string, details?: unknown) => ApiError;
35
+ /**
36
+ * Anything thrown by a handler, as the `ApiError` the caller sees.
37
+ *
38
+ * Auth refusals from `@fluixi/auth` (`requireUser`, the guards) carry a 401 or 403, and a
39
+ * `code` when they have a more precise one, and are matched by shape, the way `@fluixi/start` matches them, so neither package depends on the other.
40
+ */
41
+ export declare function toApiError(error: unknown): {
42
+ error: ApiError;
43
+ internal: boolean;
44
+ };
45
+ /** An `ApiError` as a JSON response. */
46
+ export declare function errorResponse(error: ApiError): Response;
47
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qFAAqF;AACrF,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAEd,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAO7E;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC7D;AAED,2FAA2F;AAC3F,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,aAAuD,CAAC;AACrH,eAAO,MAAM,YAAY,GAAI,gBAAgC,aAA+C,CAAC;AAC7G,eAAO,MAAM,SAAS,GAAI,gBAAwB,aAA4C,CAAC;AAC/F,eAAO,MAAM,QAAQ,GAAI,gBAAsB,aAA4C,CAAC;AAC5F,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,aAAoD,CAAC;AAEhH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAUjF;AAED,wCAAwC;AACxC,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAOvD"}
package/dist/errors.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One error model for every contracted route.
3
+ *
4
+ * A failure is always `{ error: { code, message, details? } }` with a matching HTTP status. The
5
+ * code is for programs, the message for people. Nothing from the server's internals reaches the
6
+ * response: an error that is not an `ApiError` becomes a 500 with a generic message, and the
7
+ * original is handed to `onError` for logging.
8
+ */
9
+ /** A failure a handler means to report, with the status and code the caller gets. */
10
+ export class ApiError extends Error {
11
+ status;
12
+ code;
13
+ details;
14
+ constructor(status, code, message, details) {
15
+ super(message);
16
+ this.name = 'ApiError';
17
+ this.status = status;
18
+ this.code = code;
19
+ this.details = details;
20
+ }
21
+ }
22
+ export const badRequest = (message, details) => new ApiError(400, 'bad_request', message, details);
23
+ export const unauthorized = (message = 'Sign in to continue.') => new ApiError(401, 'unauthorized', message);
24
+ export const forbidden = (message = 'Not allowed.') => new ApiError(403, 'forbidden', message);
25
+ export const notFound = (message = 'Not found.') => new ApiError(404, 'not_found', message);
26
+ export const conflict = (message, details) => new ApiError(409, 'conflict', message, details);
27
+ /**
28
+ * Anything thrown by a handler, as the `ApiError` the caller sees.
29
+ *
30
+ * Auth refusals from `@fluixi/auth` (`requireUser`, the guards) carry a 401 or 403, and a
31
+ * `code` when they have a more precise one, and are matched by shape, the way `@fluixi/start` matches them, so neither package depends on the other.
32
+ */
33
+ export function toApiError(error) {
34
+ if (error instanceof ApiError)
35
+ return { error, internal: false };
36
+ const { status, code, message } = (error ?? {});
37
+ if (status === 401 || status === 403) {
38
+ // A refusal that names itself (`no_active_organization`) keeps its code and message, so a
39
+ // client can tell "pick an organisation" from "not allowed".
40
+ if (typeof code === 'string' && typeof message === 'string')
41
+ return { error: new ApiError(status, code, message), internal: false };
42
+ return { error: status === 401 ? unauthorized() : forbidden(), internal: false };
43
+ }
44
+ return { error: new ApiError(500, 'internal', 'Something went wrong on our side.'), internal: true };
45
+ }
46
+ /** An `ApiError` as a JSON response. */
47
+ export function errorResponse(error) {
48
+ const body = { error: { code: error.code, message: error.message } };
49
+ if (error.details !== undefined)
50
+ body.error.details = error.details;
51
+ return new Response(JSON.stringify(body), {
52
+ status: error.status,
53
+ headers: { 'content-type': 'application/json; charset=utf-8' },
54
+ });
55
+ }
@@ -0,0 +1,42 @@
1
+ // src/errors.ts
2
+ var ApiError = class extends Error {
3
+ constructor(status, code, message, details) {
4
+ super(message);
5
+ this.name = "ApiError";
6
+ this.status = status;
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ };
11
+ var badRequest = (message, details) => new ApiError(400, "bad_request", message, details);
12
+ var unauthorized = (message = "Sign in to continue.") => new ApiError(401, "unauthorized", message);
13
+ var forbidden = (message = "Not allowed.") => new ApiError(403, "forbidden", message);
14
+ var notFound = (message = "Not found.") => new ApiError(404, "not_found", message);
15
+ var conflict = (message, details) => new ApiError(409, "conflict", message, details);
16
+ function toApiError(error) {
17
+ if (error instanceof ApiError) return { error, internal: false };
18
+ const { status, code, message } = error ?? {};
19
+ if (status === 401 || status === 403) {
20
+ if (typeof code === "string" && typeof message === "string") return { error: new ApiError(status, code, message), internal: false };
21
+ return { error: status === 401 ? unauthorized() : forbidden(), internal: false };
22
+ }
23
+ return { error: new ApiError(500, "internal", "Something went wrong on our side."), internal: true };
24
+ }
25
+ function errorResponse(error) {
26
+ const body = { error: { code: error.code, message: error.message } };
27
+ if (error.details !== void 0) body.error.details = error.details;
28
+ return new Response(JSON.stringify(body), {
29
+ status: error.status,
30
+ headers: { "content-type": "application/json; charset=utf-8" }
31
+ });
32
+ }
33
+ export {
34
+ ApiError,
35
+ badRequest,
36
+ conflict,
37
+ errorResponse,
38
+ forbidden,
39
+ notFound,
40
+ toApiError,
41
+ unauthorized
42
+ };