@mosano-product-framework/sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +827 -0
- package/README.react.md +348 -0
- package/dist/auth/claims-types.d.ts +89 -0
- package/dist/auth/claims.d.ts +125 -0
- package/dist/auth/cross-tab.d.ts +114 -0
- package/dist/auth/errors.d.ts +40 -0
- package/dist/auth/index.d.ts +18 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth-state.d.ts +93 -0
- package/dist/auth/session-manager.d.ts +253 -0
- package/dist/auth/storage.d.ts +36 -0
- package/dist/auth/tenant-directory.d.ts +59 -0
- package/dist/auth/tenant-selection.d.ts +92 -0
- package/dist/chunk-7WAV52EO.js +621 -0
- package/dist/chunk-7WAV52EO.js.map +1 -0
- package/dist/chunk-AJWM5MDZ.js +410 -0
- package/dist/chunk-AJWM5MDZ.js.map +1 -0
- package/dist/chunk-EXPYHNPV.js +212 -0
- package/dist/chunk-EXPYHNPV.js.map +1 -0
- package/dist/chunk-GPWGOYCA.js +85 -0
- package/dist/chunk-GPWGOYCA.js.map +1 -0
- package/dist/chunk-GQJ3QQPH.js +339 -0
- package/dist/chunk-GQJ3QQPH.js.map +1 -0
- package/dist/chunk-K2ELAI2X.js +64 -0
- package/dist/chunk-K2ELAI2X.js.map +1 -0
- package/dist/chunk-LRM6JJ63.js +616 -0
- package/dist/chunk-LRM6JJ63.js.map +1 -0
- package/dist/chunk-XAXFIIRT.js +959 -0
- package/dist/chunk-XAXFIIRT.js.map +1 -0
- package/dist/client/core/client-factory.d.ts +61 -0
- package/dist/client/core/client.d.ts +144 -0
- package/dist/client/core/errors.d.ts +105 -0
- package/dist/client/core/index.d.ts +9 -0
- package/dist/client/core/middleware.d.ts +67 -0
- package/dist/client/core/types.d.ts +99 -0
- package/dist/client/graphql/client.d.ts +66 -0
- package/dist/client/graphql/factory.d.ts +84 -0
- package/dist/client/graphql/operation.d.ts +24 -0
- package/dist/client/graphql/types.d.ts +60 -0
- package/dist/client/graphql/ws-client.d.ts +116 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.js +227 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/middlewares/admin-auth.d.ts +90 -0
- package/dist/client/middlewares/auth.d.ts +81 -0
- package/dist/client/middlewares/index.d.ts +12 -0
- package/dist/client/middlewares/logging.d.ts +102 -0
- package/dist/client/middlewares/retry.d.ts +138 -0
- package/dist/client/middlewares/tenant.d.ts +60 -0
- package/dist/client/middlewares/turnstile.d.ts +41 -0
- package/dist/client/peer-free.d.ts +25 -0
- package/dist/client/utils/url.d.ts +19 -0
- package/dist/identity/index.d.ts +85 -0
- package/dist/identity/index.js +6 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/types.d.ts +690 -0
- package/dist/identity/v0.d.ts +594 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/react/context.d.ts +47 -0
- package/dist/react/hooks.d.ts +120 -0
- package/dist/react/index.d.ts +19 -0
- package/dist/react/index.js +308 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +68 -0
- package/dist/react/store.d.ts +85 -0
- package/dist/storage/index.d.ts +31 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +107 -0
- package/dist/storage/v0.d.ts +120 -0
- package/package.json +99 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { MPFGraphQLClient, isSubscription } from '../chunk-GQJ3QQPH.js';
|
|
2
|
+
export { MPFGraphQLClient, buildUrl, createLoggingMiddleware, getOperationKind, isSubscription, joinPath } from '../chunk-GQJ3QQPH.js';
|
|
3
|
+
export { AdminEmptySecretError, AdminEmptyUserIDError, createAdminAuthMiddleware, createTurnstileMiddleware, issueAdminToken } from '../chunk-K2ELAI2X.js';
|
|
4
|
+
import { attachStandardMiddlewares } from '../chunk-LRM6JJ63.js';
|
|
5
|
+
export { MAX_REQUEST_ATTEMPTS, MPFClient, TENANT_HEADER, TENANT_ROLE_HEADER, attachStandardMiddlewares, clearRetryFlag, createAuthMiddleware, createHttpClient, createRetryMiddleware, createTenantMiddleware, disableRetry, executeErrorMiddlewares, executeRequestMiddlewares, executeResponseMiddlewares, getRetryAttempt, isReplayableBody, isRetryDisabled, requestRetry, shouldRetryRequest } from '../chunk-LRM6JJ63.js';
|
|
6
|
+
import { MPFAPIError } from '../chunk-AJWM5MDZ.js';
|
|
7
|
+
export { MPFAPIError, MPFAuthError, MPFError, MPFNetworkError, MPFTimeoutError, MPFValidationError, isMPFAPIError, isMPFAuthError, isMPFError, isMPFNetworkError, isMPFValidationError } from '../chunk-AJWM5MDZ.js';
|
|
8
|
+
|
|
9
|
+
// src/client/graphql/ws-client.ts
|
|
10
|
+
async function loadCreateClient() {
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import('graphql-ws');
|
|
13
|
+
return mod.createClient;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"[MPF SDK] GraphQL 'websocket'/'hybrid' mode requires the optional 'graphql-ws' and 'graphql' packages. Install them: pnpm add graphql-ws graphql",
|
|
17
|
+
{ cause: error instanceof Error ? error : void 0 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createGraphQLWsTransport(options) {
|
|
22
|
+
const {
|
|
23
|
+
url,
|
|
24
|
+
tokenProvider,
|
|
25
|
+
tenantProvider,
|
|
26
|
+
webSocketImpl,
|
|
27
|
+
retryAttempts = 5,
|
|
28
|
+
lazy = true,
|
|
29
|
+
createClient: injectedCreateClient
|
|
30
|
+
} = options;
|
|
31
|
+
let clientPromise = null;
|
|
32
|
+
async function buildClient() {
|
|
33
|
+
const createClient = injectedCreateClient ?? await loadCreateClient();
|
|
34
|
+
return createClient({
|
|
35
|
+
url,
|
|
36
|
+
lazy,
|
|
37
|
+
retryAttempts,
|
|
38
|
+
...webSocketImpl ? { webSocketImpl } : {},
|
|
39
|
+
connectionParams: async () => {
|
|
40
|
+
const token = tokenProvider ? await tokenProvider.getAccessToken() : null;
|
|
41
|
+
const selection = tenantProvider?.get();
|
|
42
|
+
return {
|
|
43
|
+
headers: {
|
|
44
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
45
|
+
...selection?.tenant ? { "X-MPF-Tenant": selection.tenant } : {},
|
|
46
|
+
...selection?.role ? { "X-MPF-Tenant-Role": selection.role } : {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function getClient() {
|
|
53
|
+
if (!clientPromise) {
|
|
54
|
+
clientPromise = buildClient();
|
|
55
|
+
}
|
|
56
|
+
return clientPromise;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
subscribe(document, variables, sink) {
|
|
60
|
+
let unsubscribed = false;
|
|
61
|
+
let disposer;
|
|
62
|
+
getClient().then(
|
|
63
|
+
(client) => {
|
|
64
|
+
if (unsubscribed) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
disposer = client.subscribe(
|
|
68
|
+
{
|
|
69
|
+
query: typeof document === "string" ? document : document.toString(),
|
|
70
|
+
variables
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
next: (msg) => {
|
|
74
|
+
sink.next({
|
|
75
|
+
data: msg.data ?? void 0,
|
|
76
|
+
errors: msg.errors
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
error: (err) => sink.error(err),
|
|
80
|
+
complete: () => sink.complete()
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
(err) => sink.error(err)
|
|
85
|
+
);
|
|
86
|
+
return () => {
|
|
87
|
+
unsubscribed = true;
|
|
88
|
+
disposer?.();
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
reconnect() {
|
|
92
|
+
if (!clientPromise) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
void clientPromise.then((client) => client.terminate());
|
|
96
|
+
},
|
|
97
|
+
dispose() {
|
|
98
|
+
const promise = clientPromise;
|
|
99
|
+
clientPromise = null;
|
|
100
|
+
void promise?.then((client) => client.dispose());
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/client/graphql/factory.ts
|
|
106
|
+
function deriveWsUrl(baseUrl, endpoint) {
|
|
107
|
+
const httpUrl = new URL(`${baseUrl.replace(/\/+$/, "")}${endpoint}`);
|
|
108
|
+
httpUrl.protocol = httpUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
109
|
+
return httpUrl.toString();
|
|
110
|
+
}
|
|
111
|
+
function createGraphQLClient(options) {
|
|
112
|
+
const {
|
|
113
|
+
tokenProvider,
|
|
114
|
+
tenantProvider,
|
|
115
|
+
endpoint = "/graphql",
|
|
116
|
+
mode = "hybrid",
|
|
117
|
+
wsUrl,
|
|
118
|
+
webSocketImpl,
|
|
119
|
+
wsRetryAttempts,
|
|
120
|
+
wsCreateClient,
|
|
121
|
+
...clientConfig
|
|
122
|
+
} = options;
|
|
123
|
+
const httpConfig = { ...clientConfig, endpoint };
|
|
124
|
+
const httpClient = new MPFGraphQLClient(httpConfig);
|
|
125
|
+
attachStandardMiddlewares(httpClient, { tokenProvider, tenantProvider });
|
|
126
|
+
let wsTransport = null;
|
|
127
|
+
function getWsTransport() {
|
|
128
|
+
if (mode === "individual-requests") {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"[MPF SDK] subscribe() requires mode: 'websocket' or 'hybrid' \u2014 this client was created with mode: 'individual-requests', which has no transport for subscriptions."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (!wsTransport) {
|
|
134
|
+
wsTransport = createGraphQLWsTransport({
|
|
135
|
+
url: wsUrl ?? deriveWsUrl(clientConfig.baseUrl, endpoint),
|
|
136
|
+
tokenProvider,
|
|
137
|
+
tenantProvider,
|
|
138
|
+
webSocketImpl,
|
|
139
|
+
...wsRetryAttempts !== void 0 ? { retryAttempts: wsRetryAttempts } : {},
|
|
140
|
+
...wsCreateClient ? { createClient: wsCreateClient } : {},
|
|
141
|
+
// 'hybrid' must not open a socket until subscribe() is called; the
|
|
142
|
+
// transport's own laziness gives us that for free either way.
|
|
143
|
+
lazy: true
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return wsTransport;
|
|
147
|
+
}
|
|
148
|
+
function executeOverWs(document, variables) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
let unsubscribe = () => {
|
|
152
|
+
};
|
|
153
|
+
unsubscribe = getWsTransport().subscribe(document, variables, {
|
|
154
|
+
next: (result) => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
unsubscribe();
|
|
158
|
+
if (result.errors && result.errors.length > 0) {
|
|
159
|
+
const first = result.errors[0];
|
|
160
|
+
reject(
|
|
161
|
+
new MPFAPIError(
|
|
162
|
+
first.message,
|
|
163
|
+
first.extensions?.code ?? "GRAPHQL_ERROR",
|
|
164
|
+
200,
|
|
165
|
+
result.errors
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (result.data === void 0 || result.data === null) {
|
|
171
|
+
reject(new MPFAPIError("No data returned from GraphQL operation", "NO_DATA", 200));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
resolve(result.data);
|
|
175
|
+
},
|
|
176
|
+
error: (err) => {
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
reject(err);
|
|
180
|
+
},
|
|
181
|
+
complete: () => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
reject(new MPFAPIError("Operation completed with no result", "NO_DATA", 200));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
async query(document, variables, requestOptions) {
|
|
191
|
+
if (mode === "websocket") {
|
|
192
|
+
return executeOverWs(document, variables);
|
|
193
|
+
}
|
|
194
|
+
return httpClient.query(document, variables, requestOptions);
|
|
195
|
+
},
|
|
196
|
+
async mutate(document, variables, requestOptions) {
|
|
197
|
+
if (mode === "websocket") {
|
|
198
|
+
return executeOverWs(document, variables);
|
|
199
|
+
}
|
|
200
|
+
return httpClient.mutate(document, variables, requestOptions);
|
|
201
|
+
},
|
|
202
|
+
subscribe(document, variables, handlers) {
|
|
203
|
+
const query = typeof document === "string" ? document : document.toString();
|
|
204
|
+
if (!isSubscription(query)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"[MPF SDK] subscribe() was called with a document whose operation is not a 'subscription'. Use query()/mutate() for those."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return getWsTransport().subscribe(document, variables, {
|
|
210
|
+
next: handlers.onData,
|
|
211
|
+
error: (err) => handlers.onError?.(err),
|
|
212
|
+
complete: () => handlers.onComplete?.()
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
reconnectWebSocket() {
|
|
216
|
+
wsTransport?.reconnect();
|
|
217
|
+
},
|
|
218
|
+
dispose() {
|
|
219
|
+
wsTransport?.dispose();
|
|
220
|
+
wsTransport = null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { createGraphQLClient, createGraphQLWsTransport };
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
227
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/graphql/ws-client.ts","../../src/client/graphql/factory.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `graphql-ws` transport, wired to the SAME `tokenProvider`/`tenantProvider`\n * every REST and HTTP-GraphQL client uses.\n *\n * This is meant to be a project-wide standard, not a go-hasura special case:\n * any microservice exposing `graphql-ws` subscriptions (Hasura's, or a Go\n * service's own) authenticates the same way every other MPF client does —\n * `connectionParams` carries the identical `Authorization: Bearer <jwt>` (plus\n * `X-MPF-Tenant`/`X-MPF-Tenant-Role` when a tenant is selected) that the HTTP\n * middlewares put on REST/GraphQL-over-HTTP requests. A service just needs to\n * read those three keys off the `connection_init` payload at its own webhook\n * auth boundary — no new token format.\n *\n * They are carried under a `headers` key of the `connection_init` payload —\n * `{ headers: { Authorization, 'X-MPF-Tenant', ... } }` — NOT flat at the top\n * level. That is what Hasura reads: over the websocket path it takes\n * `payload.headers` and replays those as the request headers it hands to the\n * auth webhook (hasura-authd). Flat top-level keys are silently ignored, so\n * the socket connects and every subscription then fails as unauthenticated —\n * verified end-to-end against a live stack in\n * `e2e/tests/go-identity/graphql-subscriptions.e2e.test.ts`. It is also the\n * shape the wider `graphql-ws` ecosystem uses for header-style auth, so a Go\n * service reading `payload.headers` stays consistent with both.\n */\n\nimport type { TokenProvider } from '../core/types.js';\nimport type { TenantSelectionStore } from '../../auth/tenant-selection.js';\nimport type { GraphQLError, GraphQLResponse } from './types.js';\n\n/**\n * The subset of a `graphql-ws` `Client` this module uses. Kept as our own\n * interface (not `import type { Client } from 'graphql-ws'`) so this file\n * never has a hard, non-dynamic dependency on the `graphql-ws` types either —\n * matches `cross-tab.ts`'s `LockManagerLike`/`BroadcastChannelLike` pattern.\n */\nexport interface GraphQLWsClientLike {\n subscribe<TData = unknown>(\n payload: { query: string; variables?: Record<string, unknown> },\n sink: {\n next: (value: { data?: TData | null; errors?: readonly unknown[] }) => void;\n error: (err: unknown) => void;\n complete: () => void;\n },\n ): () => void;\n terminate(): void;\n dispose(): void | Promise<void>;\n}\n\n/** Options `createGraphQLWsTransport` forwards to `graphql-ws`'s `createClient`. */\nexport interface GraphQLWsCreateClientParams {\n url: string;\n lazy: boolean;\n retryAttempts: number;\n webSocketImpl?: unknown;\n /**\n * Resolved per connection attempt. Shape is `{ headers: { ... } }` — see\n * the module doc for why the auth headers are nested rather than flat.\n */\n connectionParams: () => Promise<{ headers: Record<string, string> }>;\n}\n\nexport interface GraphQLWsClientOptions {\n /** `ws://` or `wss://` endpoint. */\n url: string;\n /** Same token provider passed to the HTTP transport — see module docs. */\n tokenProvider?: TokenProvider;\n /** Same tenant provider passed to the HTTP transport. */\n tenantProvider?: TenantSelectionStore;\n /**\n * `WebSocket` implementation for non-browser environments (Node has no\n * global `WebSocket` before v22). Pass the `ws` package's export there.\n */\n webSocketImpl?: unknown;\n /** Reconnect attempts before giving up on a dropped socket. @default 5 */\n retryAttempts?: number;\n /**\n * Open the socket only when the first subscription starts, close it when\n * the last one ends. @default true — matches `hybrid` mode's \"no socket\n * until a subscription shows up\" contract.\n */\n lazy?: boolean;\n /**\n * Injectable `graphql-ws` `createClient`. Defaults to a dynamic\n * `import('graphql-ws')`. Tests supply a fake here to exercise the full\n * subscribe/reconnect/dispose contract without a real socket or the\n * `graphql-ws` package — same reason `cross-tab.ts` takes an injectable\n * `locks`/`channelFactory`.\n */\n createClient?: (params: GraphQLWsCreateClientParams) => GraphQLWsClientLike;\n}\n\nexport interface GraphQLSubscriptionSink<TData> {\n next: (result: GraphQLResponse<TData>) => void;\n error: (error: unknown) => void;\n complete: () => void;\n}\n\nexport interface GraphQLWsTransport {\n /** Subscribe; returns an unsubscribe function. */\n subscribe<TData = unknown, TVariables = Record<string, unknown>>(\n document: { toString(): string } | string,\n variables: TVariables | undefined,\n sink: GraphQLSubscriptionSink<TData>,\n ): () => void;\n /**\n * Force the current socket closed. `graphql-ws`'s own retry logic reopens\n * it with a FRESH `connectionParams` call (so a rotated token is picked up)\n * and resubscribes every still-registered operation automatically.\n *\n * Call this right after a `SessionManager` renewal — see\n * `createGraphQLClient`'s `mode: 'websocket' | 'hybrid'` wiring — so a live\n * subscription is never carried on a token past its renewal point.\n */\n reconnect(): void;\n /** Close the socket and release it for good. Registered subscriptions end. */\n dispose(): void;\n}\n\n/**\n * Lazily import `graphql-ws`. Kept dynamic so an app that never opens a\n * subscription never pays for the dependency: `graphql-ws` (and its `graphql`\n * peer) are optional peers of this package, the same way `react` is.\n */\nasync function loadCreateClient(): Promise<\n (params: GraphQLWsCreateClientParams) => GraphQLWsClientLike\n> {\n try {\n const mod = await import('graphql-ws');\n return mod.createClient as unknown as (\n params: GraphQLWsCreateClientParams,\n ) => GraphQLWsClientLike;\n } catch (error) {\n throw new Error(\n \"[MPF SDK] GraphQL 'websocket'/'hybrid' mode requires the optional \" +\n \"'graphql-ws' and 'graphql' packages. Install them: pnpm add graphql-ws graphql\",\n { cause: error instanceof Error ? error : undefined },\n );\n }\n}\n\n/** Create a `graphql-ws`-backed subscription transport. */\nexport function createGraphQLWsTransport(options: GraphQLWsClientOptions): GraphQLWsTransport {\n const {\n url,\n tokenProvider,\n tenantProvider,\n webSocketImpl,\n retryAttempts = 5,\n lazy = true,\n createClient: injectedCreateClient,\n } = options;\n\n let clientPromise: Promise<GraphQLWsClientLike> | null = null;\n\n async function buildClient(): Promise<GraphQLWsClientLike> {\n const createClient = injectedCreateClient ?? (await loadCreateClient());\n return createClient({\n url,\n lazy,\n retryAttempts,\n ...(webSocketImpl ? { webSocketImpl } : {}),\n connectionParams: async () => {\n const token = tokenProvider ? await tokenProvider.getAccessToken() : null;\n const selection = tenantProvider?.get();\n return {\n headers: {\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...(selection?.tenant ? { 'X-MPF-Tenant': selection.tenant } : {}),\n ...(selection?.role ? { 'X-MPF-Tenant-Role': selection.role } : {}),\n },\n };\n },\n });\n }\n\n function getClient(): Promise<GraphQLWsClientLike> {\n if (!clientPromise) {\n clientPromise = buildClient();\n }\n return clientPromise;\n }\n\n return {\n subscribe<TData, TVariables>(\n document: { toString(): string } | string,\n variables: TVariables | undefined,\n sink: GraphQLSubscriptionSink<TData>,\n ) {\n let unsubscribed = false;\n let disposer: (() => void) | undefined;\n\n getClient().then(\n (client) => {\n if (unsubscribed) {\n return;\n }\n disposer = client.subscribe<TData>(\n {\n query: typeof document === 'string' ? document : document.toString(),\n variables: variables as Record<string, unknown> | undefined,\n },\n {\n next: (msg) => {\n sink.next({\n data: msg.data ?? undefined,\n errors: msg.errors as GraphQLError[] | undefined,\n });\n },\n error: (err) => sink.error(err),\n complete: () => sink.complete(),\n },\n );\n },\n (err: unknown) => sink.error(err),\n );\n\n return () => {\n unsubscribed = true;\n disposer?.();\n };\n },\n\n reconnect() {\n // No-op if no socket has ever been opened: there is nothing to force\n // closed, and building one just to immediately terminate it would be\n // wasteful (and, for 'hybrid' mode, would defeat the \"no socket until a\n // subscription shows up\" contract).\n if (!clientPromise) {\n return;\n }\n void clientPromise.then((client) => client.terminate());\n },\n\n dispose() {\n const promise = clientPromise;\n clientPromise = null;\n void promise?.then((client) => client.dispose());\n },\n };\n}\n","/**\n * `createGraphQLClient` — the GraphQL counterpart to `createAuthClient` /\n * `createStorageClient`, built on the same `attachStandardMiddlewares`\n * wiring (see `core/client-factory.ts`) so it shares one `tokenProvider` /\n * `tenantProvider` with every other client an app constructs.\n *\n * Adds one more axis specific to GraphQL: `mode`, which decides HTTP vs\n * `graphql-ws` per operation. This is a deliberately small stand-in for an\n * Apollo-Link-style split link — enough to route by operation kind, not a\n * general link-chaining system. If usage grows past \"route by operation\n * kind\" (retry links, batching links, etc.) that is the point to reach for\n * an actual link abstraction; this does not try to anticipate that.\n */\n\nimport { attachStandardMiddlewares, type HttpClientOptions } from '../core/client-factory.js';\nimport { MPFAPIError } from '../core/errors.js';\nimport { MPFGraphQLClient, type GraphQLClientConfig } from './client.js';\nimport {\n createGraphQLWsTransport,\n type GraphQLWsTransport,\n type GraphQLWsCreateClientParams,\n type GraphQLWsClientLike,\n} from './ws-client.js';\nimport { isSubscription } from './operation.js';\nimport type {\n TypedDocumentNode,\n GraphQLRequestOptions,\n GraphQLResponse,\n GraphQLError,\n} from './types.js';\n\n/**\n * How a `createGraphQLClient` routes operations.\n *\n * - `'individual-requests'` — every operation (query/mutation) goes over\n * plain HTTP POST. No socket is ever opened; `subscribe()` throws.\n * - `'websocket'` — every operation, including query/mutation, goes over one\n * `graphql-ws` connection.\n * - `'hybrid'` (default) — starts in `'individual-requests'` behavior.\n * Query/mutation always stay on HTTP (cheaper: no connection to keep\n * alive for the common case). The FIRST `subscribe()` call lazily opens\n * the `graphql-ws` connection; every subscription after that reuses it.\n */\nexport type GraphQLConnectionMode = 'individual-requests' | 'websocket' | 'hybrid';\n\nexport interface GraphQLClientOptions extends HttpClientOptions {\n /** GraphQL HTTP endpoint path. @default '/graphql' */\n endpoint?: string;\n /** @default 'hybrid' */\n mode?: GraphQLConnectionMode;\n /**\n * `ws://`/`wss://` endpoint for the `graphql-ws` transport. Defaults to\n * `baseUrl` with `http(s)` swapped for `ws(s)` and `endpoint` appended —\n * correct for Hasura, which serves GraphQL-over-HTTP and `graphql-ws` on\n * the same path. Override for a service that splits them.\n */\n wsUrl?: string;\n /** `WebSocket` implementation for non-browser runtimes (e.g. the `ws` package in Node). */\n webSocketImpl?: unknown;\n /** `graphql-ws` reconnect attempts. @default 5 */\n wsRetryAttempts?: number;\n /**\n * Injectable `graphql-ws` `createClient`, forwarded to\n * {@link createGraphQLWsTransport}. Lets tests exercise `'websocket'`/\n * `'hybrid'` routing with a fake socket instead of a real one — see\n * `ws-client.ts`'s `GraphQLWsClientOptions.createClient` doc.\n */\n wsCreateClient?: (params: GraphQLWsCreateClientParams) => GraphQLWsClientLike;\n}\n\nexport interface GraphQLSubscriptionHandlers<TData> {\n onData: (result: GraphQLResponse<TData>) => void;\n onError?: (error: unknown) => void;\n onComplete?: () => void;\n}\n\nexport interface MPFGraphQLHybridClient {\n query<TData, TVariables = Record<string, unknown>>(\n document: TypedDocumentNode<TData, TVariables> | string,\n variables?: TVariables,\n options?: GraphQLRequestOptions,\n ): Promise<TData>;\n mutate<TData, TVariables = Record<string, unknown>>(\n document: TypedDocumentNode<TData, TVariables> | string,\n variables?: TVariables,\n options?: GraphQLRequestOptions,\n ): Promise<TData>;\n /**\n * Subscribe to a `subscription` document. Throws synchronously in\n * `'individual-requests'` mode — that mode has no transport for it.\n *\n * @returns An unsubscribe function.\n */\n subscribe<TData, TVariables = Record<string, unknown>>(\n document: TypedDocumentNode<TData, TVariables> | string,\n variables: TVariables | undefined,\n handlers: GraphQLSubscriptionHandlers<TData>,\n ): () => void;\n /**\n * Force the `graphql-ws` socket closed; it reopens with a fresh token and\n * resubscribes every live subscription. Wire this to fire on every token\n * renewal — e.g. from `SessionManagerOptions.onTokensChanged` — so a\n * subscription never outlives the token it was opened with. A no-op if no\n * socket has been opened yet (nothing to reconnect).\n */\n reconnectWebSocket(): void;\n /** Release the HTTP client's resources (none currently) and close any open socket. */\n dispose(): void;\n}\n\nfunction deriveWsUrl(baseUrl: string, endpoint: string): string {\n const httpUrl = new URL(`${baseUrl.replace(/\\/+$/, '')}${endpoint}`);\n httpUrl.protocol = httpUrl.protocol === 'https:' ? 'wss:' : 'ws:';\n return httpUrl.toString();\n}\n\n/**\n * Create a GraphQL client with the standard auth/tenant middlewares wired in\n * and a `mode`-selected HTTP/`graphql-ws` transport.\n */\nexport function createGraphQLClient(options: GraphQLClientOptions): MPFGraphQLHybridClient {\n const {\n tokenProvider,\n tenantProvider,\n endpoint = '/graphql',\n mode = 'hybrid',\n wsUrl,\n webSocketImpl,\n wsRetryAttempts,\n wsCreateClient,\n ...clientConfig\n } = options;\n\n const httpConfig: GraphQLClientConfig = { ...clientConfig, endpoint };\n const httpClient = new MPFGraphQLClient(httpConfig);\n attachStandardMiddlewares(httpClient, { tokenProvider, tenantProvider });\n\n let wsTransport: GraphQLWsTransport | null = null;\n\n function getWsTransport(): GraphQLWsTransport {\n if (mode === 'individual-requests') {\n throw new Error(\n \"[MPF SDK] subscribe() requires mode: 'websocket' or 'hybrid' — this client was \" +\n \"created with mode: 'individual-requests', which has no transport for subscriptions.\",\n );\n }\n if (!wsTransport) {\n wsTransport = createGraphQLWsTransport({\n url: wsUrl ?? deriveWsUrl(clientConfig.baseUrl, endpoint),\n tokenProvider,\n tenantProvider,\n webSocketImpl,\n ...(wsRetryAttempts !== undefined ? { retryAttempts: wsRetryAttempts } : {}),\n ...(wsCreateClient ? { createClient: wsCreateClient } : {}),\n // 'hybrid' must not open a socket until subscribe() is called; the\n // transport's own laziness gives us that for free either way.\n lazy: true,\n });\n }\n return wsTransport;\n }\n\n function executeOverWs<TData, TVariables>(\n document: TypedDocumentNode<TData, TVariables> | string,\n variables: TVariables | undefined,\n ): Promise<TData> {\n return new Promise<TData>((resolve, reject) => {\n let settled = false;\n let unsubscribe: () => void = () => {};\n unsubscribe = getWsTransport().subscribe<TData, TVariables>(document, variables, {\n next: (result) => {\n if (settled) return;\n settled = true;\n unsubscribe();\n if (result.errors && result.errors.length > 0) {\n const first: GraphQLError = result.errors[0];\n reject(\n new MPFAPIError(\n first.message,\n (first.extensions?.code as string | undefined) ?? 'GRAPHQL_ERROR',\n 200,\n result.errors,\n ),\n );\n return;\n }\n if (result.data === undefined || result.data === null) {\n reject(new MPFAPIError('No data returned from GraphQL operation', 'NO_DATA', 200));\n return;\n }\n resolve(result.data);\n },\n error: (err) => {\n if (settled) return;\n settled = true;\n reject(err);\n },\n complete: () => {\n if (settled) return;\n settled = true;\n reject(new MPFAPIError('Operation completed with no result', 'NO_DATA', 200));\n },\n });\n });\n }\n\n return {\n async query(document, variables, requestOptions) {\n if (mode === 'websocket') {\n return executeOverWs(document, variables);\n }\n return httpClient.query(document, variables, requestOptions);\n },\n\n async mutate(document, variables, requestOptions) {\n if (mode === 'websocket') {\n return executeOverWs(document, variables);\n }\n return httpClient.mutate(document, variables, requestOptions);\n },\n\n subscribe(document, variables, handlers) {\n const query = typeof document === 'string' ? document : document.toString();\n if (!isSubscription(query)) {\n throw new Error(\n '[MPF SDK] subscribe() was called with a document whose operation is not a ' +\n \"'subscription'. Use query()/mutate() for those.\",\n );\n }\n return getWsTransport().subscribe(document, variables, {\n next: handlers.onData,\n error: (err) => handlers.onError?.(err),\n complete: () => handlers.onComplete?.(),\n });\n },\n\n reconnectWebSocket() {\n wsTransport?.reconnect();\n },\n\n dispose() {\n wsTransport?.dispose();\n wsTransport = null;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Middleware } from '../core/middleware.js';
|
|
2
|
+
/**
|
|
3
|
+
* Impersonation payload embedded in an admin token.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the Go `ActOnBehalfOf` struct
|
|
6
|
+
* (`internal/go-chi-middleware-auth/types.go`), which the Go verifier reads
|
|
7
|
+
* from the `act_on_behalf_of` JWT claim.
|
|
8
|
+
*/
|
|
9
|
+
export interface AdminActOnBehalfOf {
|
|
10
|
+
/** The subject (user) the admin token acts on behalf of. Required. */
|
|
11
|
+
userId: string;
|
|
12
|
+
/** Optionally scopes the impersonation to a tenant. */
|
|
13
|
+
onTenantId?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Error thrown when an admin token is minted without a signing secret. */
|
|
16
|
+
export declare class AdminEmptySecretError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
/** Error thrown when an admin token is minted without a subject user id. */
|
|
20
|
+
export declare class AdminEmptyUserIDError extends Error {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mints an HS256-signed Admin JWT that impersonates `actOnBehalfOf`.
|
|
25
|
+
*
|
|
26
|
+
* The minted token interoperates byte-for-byte with the Go verifier in
|
|
27
|
+
* `internal/go-chi-middleware-auth/auth.go` (`parseAdmin`):
|
|
28
|
+
* - Protected header `{ alg: 'HS256' }`.
|
|
29
|
+
* - Required `exp` claim (`now + ttlSeconds`), which the Go side enforces via
|
|
30
|
+
* `jwt.WithExpirationRequired()`.
|
|
31
|
+
* - An `iat` claim of `now`.
|
|
32
|
+
* - An `act_on_behalf_of` object claim with `user_id` (required) and an
|
|
33
|
+
* optional `on_tenant_id`, matching the Go `ActOnBehalfOf` JSON tags.
|
|
34
|
+
*
|
|
35
|
+
* SECURITY: Minting an admin token requires the shared `ADMIN_JWT_SECRET`.
|
|
36
|
+
* This secret grants the ability to impersonate ANY user, so it MUST ONLY be
|
|
37
|
+
* used server-side or by trusted backend services. NEVER ship the secret to a
|
|
38
|
+
* browser or any untrusted client, and never embed it in frontend bundles.
|
|
39
|
+
*
|
|
40
|
+
* @param secret - The shared HS256 signing secret (`ADMIN_JWT_SECRET`).
|
|
41
|
+
* @param actOnBehalfOf - The principal to impersonate.
|
|
42
|
+
* @param ttlSeconds - Token lifetime in seconds (added to the current time).
|
|
43
|
+
* @returns The signed compact JWT string.
|
|
44
|
+
* @throws {AdminEmptySecretError} when `secret` is empty.
|
|
45
|
+
* @throws {AdminEmptyUserIDError} when `actOnBehalfOf.userId` is empty.
|
|
46
|
+
*/
|
|
47
|
+
export declare function issueAdminToken(secret: string, actOnBehalfOf: AdminActOnBehalfOf, ttlSeconds: number): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Options for the admin auth middleware.
|
|
50
|
+
*
|
|
51
|
+
* Exactly one of `token` (pre-minted) or `config` (mint-per-request) must be
|
|
52
|
+
* provided.
|
|
53
|
+
*/
|
|
54
|
+
export type AdminAuthMiddlewareOptions = {
|
|
55
|
+
/** A pre-minted admin JWT to attach as `Authorization: Admin <token>`. */
|
|
56
|
+
token: string;
|
|
57
|
+
config?: never;
|
|
58
|
+
} | {
|
|
59
|
+
token?: never;
|
|
60
|
+
/**
|
|
61
|
+
* Per-request minting config (analogous to Go's `AdminRoundTripper`):
|
|
62
|
+
* a fresh admin token is minted for every request.
|
|
63
|
+
*/
|
|
64
|
+
config: {
|
|
65
|
+
/** The shared HS256 signing secret (`ADMIN_JWT_SECRET`). */
|
|
66
|
+
secret: string;
|
|
67
|
+
/** The principal to impersonate. */
|
|
68
|
+
actOnBehalfOf: AdminActOnBehalfOf;
|
|
69
|
+
/** Token lifetime in seconds. */
|
|
70
|
+
ttlSeconds: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Creates an admin authentication middleware that stamps an
|
|
75
|
+
* `Authorization: Admin <token>` header on outgoing requests.
|
|
76
|
+
*
|
|
77
|
+
* Reuses {@link createAuthMiddleware} with `tokenType: 'Admin'`. Two ergonomics
|
|
78
|
+
* are supported:
|
|
79
|
+
* - `{ token }`: attach a pre-minted admin token.
|
|
80
|
+
* - `{ config }`: mint a fresh admin token per request (like Go's
|
|
81
|
+
* `AdminRoundTripper`).
|
|
82
|
+
*
|
|
83
|
+
* SECURITY: When using `config`, the `ADMIN_JWT_SECRET` is held in memory and
|
|
84
|
+
* used to mint tokens. Only construct this middleware server-side / in trusted
|
|
85
|
+
* services. NEVER ship the secret to a browser.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Either a pre-minted `token` or a minting `config`.
|
|
88
|
+
* @returns A middleware instance.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createAdminAuthMiddleware(options: AdminAuthMiddlewareOptions): Middleware;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Middleware } from '../core/middleware.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the auth middleware.
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthMiddlewareOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Function to get the current access token.
|
|
8
|
+
* Can be sync or async (e.g., reading from localStorage or refreshing token).
|
|
9
|
+
*
|
|
10
|
+
* @returns The access token, or null if not authenticated
|
|
11
|
+
*/
|
|
12
|
+
getAccessToken: () => string | null | Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Called when a 401 is final — i.e. a token WAS attached and renewal either
|
|
15
|
+
* was not possible, failed, or had already been tried for this request.
|
|
16
|
+
*
|
|
17
|
+
* This is the "log the user out" hook. It is deliberately NOT called for a
|
|
18
|
+
* 401 on a request that carried no token (a wrong password must not log you
|
|
19
|
+
* out of a session you do have), nor for the first 401 when `onUnauthorized`
|
|
20
|
+
* successfully renews.
|
|
21
|
+
*/
|
|
22
|
+
onTokenExpired?: () => void | Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Called on the first 401 of a request that DID carry a token.
|
|
25
|
+
*
|
|
26
|
+
* Should attempt a session renewal and resolve `true` when a fresh token is
|
|
27
|
+
* now available, `false` otherwise. On `true` the request is retried once,
|
|
28
|
+
* with headers rebuilt so the new token is picked up.
|
|
29
|
+
*
|
|
30
|
+
* Capped at one call per logical request by `__authRenewAttempted`, so a
|
|
31
|
+
* second 401 falls through to `onTokenExpired` instead of looping.
|
|
32
|
+
*/
|
|
33
|
+
onUnauthorized?: () => Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* The token type prefix for the Authorization header.
|
|
36
|
+
* @default 'Bearer'
|
|
37
|
+
*/
|
|
38
|
+
tokenType?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Custom header name for the authorization token.
|
|
41
|
+
* @default 'Authorization'
|
|
42
|
+
*/
|
|
43
|
+
headerName?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates an authentication middleware that adds authorization headers to requests.
|
|
47
|
+
*
|
|
48
|
+
* The middleware:
|
|
49
|
+
* - Adds an Authorization header with the token from getAccessToken()
|
|
50
|
+
* - Calls onTokenExpired() when a 401 response is received
|
|
51
|
+
*
|
|
52
|
+
* @param options - Configuration options for the auth middleware
|
|
53
|
+
* @returns A middleware instance
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const authMiddleware = createAuthMiddleware({
|
|
58
|
+
* getAccessToken: () => localStorage.getItem('accessToken'),
|
|
59
|
+
* onTokenExpired: () => {
|
|
60
|
+
* // Redirect to login or refresh token
|
|
61
|
+
* window.location.href = '/login';
|
|
62
|
+
* }
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // With async token retrieval
|
|
69
|
+
* const authMiddleware = createAuthMiddleware({
|
|
70
|
+
* getAccessToken: async () => {
|
|
71
|
+
* const session = await authService.getSession();
|
|
72
|
+
* return session?.accessToken ?? null;
|
|
73
|
+
* },
|
|
74
|
+
* tokenType: 'Bearer',
|
|
75
|
+
* onTokenExpired: async () => {
|
|
76
|
+
* await authService.refreshToken();
|
|
77
|
+
* }
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function createAuthMiddleware(options: AuthMiddlewareOptions): Middleware;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in middlewares for the MPF SDK.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export { createAuthMiddleware, type AuthMiddlewareOptions } from './auth.js';
|
|
7
|
+
export { createAdminAuthMiddleware, issueAdminToken, AdminEmptySecretError, AdminEmptyUserIDError, type AdminActOnBehalfOf, type AdminAuthMiddlewareOptions, } from './admin-auth.js';
|
|
8
|
+
export { createRetryMiddleware, shouldRetryRequest, getRetryAttempt, clearRetryFlag, disableRetry, isRetryDisabled, isReplayableBody, type RetryMiddlewareOptions, } from './retry.js';
|
|
9
|
+
export { createLoggingMiddleware, type LoggingMiddlewareOptions, type Logger, } from './logging.js';
|
|
10
|
+
export { requestRetry } from './retry.js';
|
|
11
|
+
export { createTurnstileMiddleware, type TurnstileMiddlewareOptions } from './turnstile.js';
|
|
12
|
+
export { createTenantMiddleware, TENANT_HEADER, TENANT_ROLE_HEADER, type TenantMiddlewareOptions, } from './tenant.js';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Middleware } from '../core/middleware.js';
|
|
2
|
+
import type { RequestContext, ResponseContext } from '../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Logger interface compatible with console and custom loggers.
|
|
5
|
+
*/
|
|
6
|
+
export interface Logger {
|
|
7
|
+
log: (message: string, ...args: unknown[]) => void;
|
|
8
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for the logging middleware.
|
|
12
|
+
*/
|
|
13
|
+
export interface LoggingMiddlewareOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Logger instance to use for output.
|
|
16
|
+
* Defaults to console.
|
|
17
|
+
*/
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to log request/response bodies.
|
|
21
|
+
* Default is false for security (bodies may contain sensitive data).
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
logBody?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Prefix for all log messages.
|
|
27
|
+
* @default '[MPF SDK]'
|
|
28
|
+
*/
|
|
29
|
+
prefix?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to log request headers.
|
|
32
|
+
* Sensitive headers like Authorization are always redacted.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
logHeaders?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* List of header names to redact from logs (case-insensitive).
|
|
38
|
+
* @default ['authorization', 'cookie', 'x-api-key', 'x-auth-token']
|
|
39
|
+
*/
|
|
40
|
+
redactHeaders?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Minimum duration in ms to log. Requests faster than this are not logged.
|
|
43
|
+
* Useful for reducing log volume in high-traffic scenarios.
|
|
44
|
+
* @default 0 (log all requests)
|
|
45
|
+
*/
|
|
46
|
+
minDuration?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Whether to include timestamp in log messages.
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
includeTimestamp?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Custom log formatter for requests.
|
|
54
|
+
* If provided, overrides the default request logging format.
|
|
55
|
+
*/
|
|
56
|
+
formatRequest?: (ctx: RequestContext) => string;
|
|
57
|
+
/**
|
|
58
|
+
* Custom log formatter for responses.
|
|
59
|
+
* If provided, overrides the default response logging format.
|
|
60
|
+
*/
|
|
61
|
+
formatResponse?: <T>(ctx: ResponseContext<T>) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Custom log formatter for errors.
|
|
64
|
+
* If provided, overrides the default error logging format.
|
|
65
|
+
*/
|
|
66
|
+
formatError?: (error: Error, ctx: RequestContext) => string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Creates a logging middleware for debugging and monitoring.
|
|
70
|
+
*
|
|
71
|
+
* The middleware logs:
|
|
72
|
+
* - Request method, URL, and optionally body/headers
|
|
73
|
+
* - Response status, URL, and duration
|
|
74
|
+
* - Errors with context
|
|
75
|
+
*
|
|
76
|
+
* Sensitive headers (Authorization, Cookie, etc.) are automatically redacted.
|
|
77
|
+
*
|
|
78
|
+
* @param options - Configuration options for the logging middleware
|
|
79
|
+
* @returns A middleware instance
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const loggingMiddleware = createLoggingMiddleware({
|
|
84
|
+
* prefix: '[MyApp API]',
|
|
85
|
+
* logBody: false,
|
|
86
|
+
* logHeaders: true,
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // With custom logger
|
|
93
|
+
* const loggingMiddleware = createLoggingMiddleware({
|
|
94
|
+
* logger: {
|
|
95
|
+
* log: (msg, ...args) => myLogger.info(msg, ...args),
|
|
96
|
+
* error: (msg, ...args) => myLogger.error(msg, ...args),
|
|
97
|
+
* },
|
|
98
|
+
* minDuration: 100, // Only log requests taking > 100ms
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function createLoggingMiddleware(options?: LoggingMiddlewareOptions): Middleware;
|