@keelapi/sdk 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/dist/examples/express.d.ts +1 -0
  4. package/dist/examples/express.js +33 -0
  5. package/dist/examples/next-route.d.ts +6 -0
  6. package/dist/examples/next-route.js +29 -0
  7. package/dist/examples/permit.d.ts +1 -0
  8. package/dist/examples/permit.js +49 -0
  9. package/dist/src/client.d.ts +28 -0
  10. package/dist/src/client.js +48 -0
  11. package/dist/src/clients/execute.d.ts +8 -0
  12. package/dist/src/clients/execute.js +15 -0
  13. package/dist/src/clients/executions.d.ts +9 -0
  14. package/dist/src/clients/executions.js +22 -0
  15. package/dist/src/clients/jobs.d.ts +8 -0
  16. package/dist/src/clients/jobs.js +15 -0
  17. package/dist/src/clients/permits.d.ts +18 -0
  18. package/dist/src/clients/permits.js +72 -0
  19. package/dist/src/clients/proxy.d.ts +10 -0
  20. package/dist/src/clients/proxy.js +24 -0
  21. package/dist/src/clients/requests.d.ts +7 -0
  22. package/dist/src/clients/requests.js +12 -0
  23. package/dist/src/errors.d.ts +7 -0
  24. package/dist/src/errors.js +14 -0
  25. package/dist/src/http.d.ts +48 -0
  26. package/dist/src/http.js +303 -0
  27. package/dist/src/index.d.ts +15 -0
  28. package/dist/src/index.js +25 -0
  29. package/dist/src/middleware/express.d.ts +4 -0
  30. package/dist/src/middleware/express.js +26 -0
  31. package/dist/src/middleware/next.d.ts +10 -0
  32. package/dist/src/middleware/next.js +26 -0
  33. package/dist/src/middleware/shared.d.ts +40 -0
  34. package/dist/src/middleware/shared.js +122 -0
  35. package/dist/src/streaming.d.ts +2 -0
  36. package/dist/src/streaming.js +82 -0
  37. package/dist/src/types.d.ts +622 -0
  38. package/dist/src/types.js +5 -0
  39. package/package.json +41 -0
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeelError = void 0;
4
+ class KeelError extends Error {
5
+ constructor(status, message, details, code, field) {
6
+ super(message);
7
+ this.name = "KeelError";
8
+ this.status = status;
9
+ this.code = code;
10
+ this.field = field;
11
+ this.details = details;
12
+ }
13
+ }
14
+ exports.KeelError = KeelError;
@@ -0,0 +1,48 @@
1
+ export type RequestIdProvider = () => string | null | undefined;
2
+ export interface RequestFreshnessOptions {
3
+ enabled: boolean;
4
+ nonceFn?: () => string;
5
+ timestampFn?: () => number;
6
+ }
7
+ export interface HttpTransportOptions {
8
+ baseUrl: string;
9
+ apiKey: string;
10
+ timeoutMs?: number;
11
+ requestId?: string;
12
+ requestIdProvider?: RequestIdProvider;
13
+ requestFreshness?: RequestFreshnessOptions;
14
+ }
15
+ export declare class HttpTransport {
16
+ private readonly baseUrl;
17
+ private readonly apiKey;
18
+ private readonly timeoutMs;
19
+ private readonly fetchImpl;
20
+ private readonly requestId?;
21
+ private readonly requestIdProvider?;
22
+ private readonly requestFreshness?;
23
+ constructor(options: HttpTransportOptions);
24
+ post<T>(path: string, body?: unknown): Promise<T>;
25
+ postWithMeta<T>(path: string, body?: unknown): Promise<{
26
+ payload: T;
27
+ request_id?: string;
28
+ }>;
29
+ get<T>(path: string, query?: Record<string, string>): Promise<T>;
30
+ getWithMeta<T>(path: string, query?: Record<string, string>): Promise<{
31
+ payload: T;
32
+ request_id?: string;
33
+ }>;
34
+ delete(path: string): Promise<void>;
35
+ postStream(path: string, body?: unknown): Promise<Response>;
36
+ getRaw(path: string, query?: Record<string, string>): Promise<Response>;
37
+ private requestJson;
38
+ private buildHeaders;
39
+ private attachFreshnessHeaders;
40
+ private resolveRequestId;
41
+ private parseJson;
42
+ private extractRequestId;
43
+ extractErrorInfo(payload: unknown, fallback: string): {
44
+ message: string;
45
+ code?: string;
46
+ field?: string;
47
+ };
48
+ }
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpTransport = void 0;
4
+ const undici_1 = require("undici");
5
+ const crypto_1 = require("crypto");
6
+ const errors_1 = require("./errors");
7
+ const DEFAULT_TIMEOUT_MS = 10000;
8
+ function resolveFetch() {
9
+ if (typeof globalThis.fetch === "function") {
10
+ return globalThis.fetch.bind(globalThis);
11
+ }
12
+ return undici_1.fetch;
13
+ }
14
+ class HttpTransport {
15
+ constructor(options) {
16
+ const { baseUrl, apiKey, timeoutMs, requestId, requestIdProvider, requestFreshness, } = options;
17
+ if (!baseUrl || !baseUrl.trim()) {
18
+ throw new Error("baseUrl is required");
19
+ }
20
+ if (!apiKey || !apiKey.trim()) {
21
+ throw new Error("apiKey is required");
22
+ }
23
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
24
+ this.apiKey = apiKey;
25
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
26
+ this.fetchImpl = resolveFetch();
27
+ this.requestId = isNonEmptyString(requestId) ? requestId : undefined;
28
+ this.requestIdProvider = requestIdProvider;
29
+ this.requestFreshness = resolveRequestFreshness(requestFreshness);
30
+ }
31
+ async post(path, body) {
32
+ const { payload } = await this.requestJson({
33
+ method: "POST",
34
+ path,
35
+ body,
36
+ });
37
+ return payload;
38
+ }
39
+ async postWithMeta(path, body) {
40
+ const { payload, requestId } = await this.requestJson({
41
+ method: "POST",
42
+ path,
43
+ body,
44
+ });
45
+ return { payload, request_id: requestId };
46
+ }
47
+ async get(path, query) {
48
+ let url = path;
49
+ if (query) {
50
+ const filtered = Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== "");
51
+ if (filtered.length > 0) {
52
+ url += "?" + new URLSearchParams(filtered).toString();
53
+ }
54
+ }
55
+ const { payload } = await this.requestJson({ method: "GET", path: url });
56
+ return payload;
57
+ }
58
+ async getWithMeta(path, query) {
59
+ let url = path;
60
+ if (query) {
61
+ const filtered = Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== "");
62
+ if (filtered.length > 0) {
63
+ url += "?" + new URLSearchParams(filtered).toString();
64
+ }
65
+ }
66
+ const { payload, requestId } = await this.requestJson({
67
+ method: "GET",
68
+ path: url,
69
+ });
70
+ return { payload, request_id: requestId };
71
+ }
72
+ async delete(path) {
73
+ await this.requestJson({
74
+ method: "DELETE",
75
+ path,
76
+ });
77
+ }
78
+ async postStream(path, body) {
79
+ const controller = new AbortController();
80
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs * 6);
81
+ try {
82
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
83
+ method: "POST",
84
+ headers: this.buildHeaders(true, body !== undefined),
85
+ body: body === undefined ? undefined : JSON.stringify(body),
86
+ signal: controller.signal,
87
+ });
88
+ if (!response.ok) {
89
+ const payload = await this.parseJson(response);
90
+ const { message, code, field } = this.extractErrorInfo(payload, response.statusText);
91
+ throw new errors_1.KeelError(response.status, message, payload, code, field);
92
+ }
93
+ return response;
94
+ }
95
+ catch (error) {
96
+ clearTimeout(timeout);
97
+ if (error instanceof errors_1.KeelError) {
98
+ throw error;
99
+ }
100
+ if (error instanceof Error && error.name === "AbortError") {
101
+ throw new errors_1.KeelError(408, `Stream timed out`);
102
+ }
103
+ throw new errors_1.KeelError(0, "Network error", error);
104
+ }
105
+ }
106
+ async getRaw(path, query) {
107
+ let url = path;
108
+ if (query) {
109
+ const filtered = Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== "");
110
+ if (filtered.length > 0) {
111
+ url += "?" + new URLSearchParams(filtered).toString();
112
+ }
113
+ }
114
+ const controller = new AbortController();
115
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
116
+ try {
117
+ const response = await this.fetchImpl(`${this.baseUrl}${url}`, {
118
+ method: "GET",
119
+ headers: this.buildHeaders(true, false),
120
+ signal: controller.signal,
121
+ });
122
+ if (!response.ok) {
123
+ const payload = await this.parseJson(response);
124
+ const { message, code, field } = this.extractErrorInfo(payload, response.statusText);
125
+ throw new errors_1.KeelError(response.status, message, payload, code, field);
126
+ }
127
+ return response;
128
+ }
129
+ catch (error) {
130
+ if (error instanceof errors_1.KeelError) {
131
+ throw error;
132
+ }
133
+ if (error instanceof Error && error.name === "AbortError") {
134
+ throw new errors_1.KeelError(408, `Request timed out after ${this.timeoutMs}ms`);
135
+ }
136
+ throw new errors_1.KeelError(0, "Network error", error);
137
+ }
138
+ finally {
139
+ clearTimeout(timeout);
140
+ }
141
+ }
142
+ async requestJson({ method, path, body, includeAuth = true, }) {
143
+ const controller = new AbortController();
144
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
145
+ try {
146
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
147
+ method,
148
+ headers: this.buildHeaders(includeAuth, body !== undefined),
149
+ body: body === undefined ? undefined : JSON.stringify(body),
150
+ signal: controller.signal,
151
+ });
152
+ const payload = await this.parseJson(response);
153
+ if (!response.ok) {
154
+ const { message, code, field } = this.extractErrorInfo(payload, response.statusText);
155
+ throw new errors_1.KeelError(response.status, message, payload, code, field);
156
+ }
157
+ return {
158
+ payload: payload,
159
+ requestId: this.extractRequestId(response, payload),
160
+ };
161
+ }
162
+ catch (error) {
163
+ if (error instanceof errors_1.KeelError) {
164
+ throw error;
165
+ }
166
+ if (error instanceof Error && error.name === "AbortError") {
167
+ throw new errors_1.KeelError(408, `Request timed out after ${this.timeoutMs}ms`);
168
+ }
169
+ throw new errors_1.KeelError(0, "Network error", error);
170
+ }
171
+ finally {
172
+ clearTimeout(timeout);
173
+ }
174
+ }
175
+ buildHeaders(includeAuth, includeContentType) {
176
+ const headers = new Headers();
177
+ if (includeContentType) {
178
+ headers.set("Content-Type", "application/json");
179
+ }
180
+ if (includeAuth) {
181
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
182
+ this.attachFreshnessHeaders(headers);
183
+ }
184
+ const requestId = this.resolveRequestId();
185
+ if (requestId) {
186
+ headers.set("X-Request-Id", requestId);
187
+ }
188
+ return headers;
189
+ }
190
+ attachFreshnessHeaders(headers) {
191
+ if (!this.requestFreshness) {
192
+ return;
193
+ }
194
+ const timestamp = resolveFreshnessTimestamp(this.requestFreshness.timestampFn);
195
+ const nonce = resolveFreshnessNonce(this.requestFreshness.nonceFn);
196
+ headers.set("X-Keel-Timestamp", String(timestamp));
197
+ headers.set("X-Keel-Nonce", nonce);
198
+ }
199
+ resolveRequestId() {
200
+ if (this.requestIdProvider) {
201
+ const requestIdFromProvider = this.requestIdProvider();
202
+ if (isNonEmptyString(requestIdFromProvider)) {
203
+ return requestIdFromProvider;
204
+ }
205
+ }
206
+ if (isNonEmptyString(this.requestId)) {
207
+ return this.requestId;
208
+ }
209
+ return undefined;
210
+ }
211
+ async parseJson(response) {
212
+ const text = await response.text();
213
+ if (!text) {
214
+ return {};
215
+ }
216
+ try {
217
+ return JSON.parse(text);
218
+ }
219
+ catch {
220
+ throw new errors_1.KeelError(response.status, "Invalid JSON response", text);
221
+ }
222
+ }
223
+ extractRequestId(response, payload) {
224
+ const requestIdFromHeader = response.headers.get("x-request-id");
225
+ if (isNonEmptyString(requestIdFromHeader)) {
226
+ return requestIdFromHeader;
227
+ }
228
+ if (payload && typeof payload === "object") {
229
+ const requestIdFromPayload = payload
230
+ .request_id;
231
+ if (isNonEmptyString(requestIdFromPayload)) {
232
+ return requestIdFromPayload;
233
+ }
234
+ }
235
+ return undefined;
236
+ }
237
+ extractErrorInfo(payload, fallback) {
238
+ if (payload && typeof payload === "object") {
239
+ const maybeError = payload.error;
240
+ if (maybeError && typeof maybeError === "object") {
241
+ const err = maybeError;
242
+ const message = typeof err.message === "string" && err.message.trim()
243
+ ? err.message
244
+ : undefined;
245
+ const code = typeof err.code === "string" && err.code.trim()
246
+ ? err.code
247
+ : undefined;
248
+ const field = typeof err.field === "string" && err.field.trim()
249
+ ? err.field
250
+ : undefined;
251
+ if (message) {
252
+ return { message, code, field };
253
+ }
254
+ }
255
+ const maybeMessage = payload.message;
256
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) {
257
+ return { message: maybeMessage };
258
+ }
259
+ }
260
+ return { message: fallback || "Request failed" };
261
+ }
262
+ }
263
+ exports.HttpTransport = HttpTransport;
264
+ function isNonEmptyString(value) {
265
+ return typeof value === "string" && value.trim().length > 0;
266
+ }
267
+ function resolveRequestFreshness(requestFreshness) {
268
+ if (!requestFreshness?.enabled) {
269
+ return undefined;
270
+ }
271
+ return {
272
+ nonceFn: requestFreshness.nonceFn ?? defaultNonce,
273
+ timestampFn: requestFreshness.timestampFn ?? defaultTimestamp,
274
+ };
275
+ }
276
+ function resolveFreshnessTimestamp(timestampFn) {
277
+ const timestamp = timestampFn();
278
+ if (Number.isFinite(timestamp) && timestamp > 0) {
279
+ return Math.floor(timestamp);
280
+ }
281
+ return defaultTimestamp();
282
+ }
283
+ function resolveFreshnessNonce(nonceFn) {
284
+ const nonce = nonceFn();
285
+ if (isNonEmptyString(nonce) && nonce.trim().length >= 16) {
286
+ return nonce.trim();
287
+ }
288
+ return fallbackNonce();
289
+ }
290
+ function defaultTimestamp() {
291
+ return Math.floor(Date.now() / 1000);
292
+ }
293
+ function defaultNonce() {
294
+ try {
295
+ return (0, crypto_1.randomUUID)().replace(/-/g, "");
296
+ }
297
+ catch {
298
+ return fallbackNonce();
299
+ }
300
+ }
301
+ function fallbackNonce() {
302
+ return (0, crypto_1.randomBytes)(16).toString("hex");
303
+ }
@@ -0,0 +1,15 @@
1
+ export { KeelClient } from "./client";
2
+ export type { KeelClientOptions, RequestFreshnessOptions, RequestIdProvider, } from "./client";
3
+ export { KeelError } from "./errors";
4
+ export { PermitsClient } from "./clients/permits";
5
+ export { ExecutionsClient } from "./clients/executions";
6
+ export { ExecuteClient } from "./clients/execute";
7
+ export { ProxyClient } from "./clients/proxy";
8
+ export { JobsClient } from "./clients/jobs";
9
+ export { RequestsClient } from "./clients/requests";
10
+ export { parseSSEStream } from "./streaming";
11
+ export { keelExpress } from "./middleware/express";
12
+ export type { KeelExpressOptions } from "./middleware/express";
13
+ export { withKeel } from "./middleware/next";
14
+ export type { KeelNextOptions, NextApiHandler, NextApiRequestLike, NextApiResponseLike, } from "./middleware/next";
15
+ export type { Action, ActionMessage, ApiKey, AuthedHealthResponse, ConditionEvaluation, ConditionMatch, ConditionOutcome, CreateApiKeyResponse, Decision, DecisionDetails, ExecuteRequest, ExecutionAssetInput, ExecutionAssetSource, ExecutionCompletedEvent, ExecutionContentDelta, ExecutionCreateRequest, ExecutionDeniedEvent, ExecutionDoneEvent, ExecutionErrorEvent, ExecutionErrorPayload, ExecutionGovernance, ExecutionInputPart, ExecutionInputType, ExecutionMessage, ExecutionMode, ExecutionOperation, ExecutionOutput, ExecutionOutputAsset, ExecutionOutputContentPart, ExecutionParametersInput, ExecutionProviderOptions, ExecutionRawEvent, ExecutionResolved, ExecutionResponse, ExecutionRole, ExecutionRoutingInput, ExecutionRoutingResult, ExecutionRoutingTarget, ExecutionStartedEvent, ExecutionStreamEvent, ExecutionStreamEventName, ExecutionTimelineExecution, ExecutionTimelinePermit, ExecutionTimelineQueue, ExecutionTimelineResponse, ExecutionTimelineRouting, ExecutionTimelineUsage, ExecutionTiming, ExecutionUsage, HealthResponse, InputDescriptor, JobCreateResponse, JobStatus, JobStatusResponse, JobSubmitRequest, ListApiKeysResponse, Metadata, PermitAttestationRequest, PermitAttestResponse, PermitAttestationOut, PermitAuditBundle, PermitAuditItem, PermitAuditListResponse, PermitConditionRule, PermitConditions, PermitDryRunReplayOutcome, PermitDryRunResponse, PermitEvidenceCreateRequest, PermitEvidenceListResponse, PermitEvidenceOut, PermitExportFormat, PermitLineageNode, PermitLineageNodeSummary, PermitLineageResponse, PermitListParams, PermitRequest, PermitResponse, PermitUsageReportRequest, PermitUsageReportResponse, PermitWithMetaResponse, RequestTimelineEvent, RequestTimelineEventSource, RequestTimelinePhase, RequestTimelineResponse, Resource, ResourceAttributes, RevokeApiKeyResponse, RoutingDecision, RoutingPriority, RoutingProvider, RoutingReasonCode, RoutingTarget, Subject, BudgetEnvelopeSummary, } from "./types";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withKeel = exports.keelExpress = exports.parseSSEStream = exports.RequestsClient = exports.JobsClient = exports.ProxyClient = exports.ExecuteClient = exports.ExecutionsClient = exports.PermitsClient = exports.KeelError = exports.KeelClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "KeelClient", { enumerable: true, get: function () { return client_1.KeelClient; } });
6
+ var errors_1 = require("./errors");
7
+ Object.defineProperty(exports, "KeelError", { enumerable: true, get: function () { return errors_1.KeelError; } });
8
+ var permits_1 = require("./clients/permits");
9
+ Object.defineProperty(exports, "PermitsClient", { enumerable: true, get: function () { return permits_1.PermitsClient; } });
10
+ var executions_1 = require("./clients/executions");
11
+ Object.defineProperty(exports, "ExecutionsClient", { enumerable: true, get: function () { return executions_1.ExecutionsClient; } });
12
+ var execute_1 = require("./clients/execute");
13
+ Object.defineProperty(exports, "ExecuteClient", { enumerable: true, get: function () { return execute_1.ExecuteClient; } });
14
+ var proxy_1 = require("./clients/proxy");
15
+ Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return proxy_1.ProxyClient; } });
16
+ var jobs_1 = require("./clients/jobs");
17
+ Object.defineProperty(exports, "JobsClient", { enumerable: true, get: function () { return jobs_1.JobsClient; } });
18
+ var requests_1 = require("./clients/requests");
19
+ Object.defineProperty(exports, "RequestsClient", { enumerable: true, get: function () { return requests_1.RequestsClient; } });
20
+ var streaming_1 = require("./streaming");
21
+ Object.defineProperty(exports, "parseSSEStream", { enumerable: true, get: function () { return streaming_1.parseSSEStream; } });
22
+ var express_1 = require("./middleware/express");
23
+ Object.defineProperty(exports, "keelExpress", { enumerable: true, get: function () { return express_1.keelExpress; } });
24
+ var next_1 = require("./middleware/next");
25
+ Object.defineProperty(exports, "withKeel", { enumerable: true, get: function () { return next_1.withKeel; } });
@@ -0,0 +1,4 @@
1
+ import type express from "express";
2
+ import { type KeelMiddlewareOptions } from "./shared";
3
+ export type KeelExpressOptions = KeelMiddlewareOptions<express.Request, express.Response>;
4
+ export declare function keelExpress(opts: KeelExpressOptions): express.RequestHandler;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keelExpress = keelExpress;
4
+ const shared_1 = require("./shared");
5
+ function keelExpress(opts) {
6
+ const client = (0, shared_1.createKeelClientForMiddleware)({
7
+ baseUrl: opts.baseUrl,
8
+ apiKey: opts.apiKey,
9
+ requestFreshness: opts.requestFreshness,
10
+ });
11
+ return async (req, res, next) => {
12
+ try {
13
+ const permitRequest = (0, shared_1.buildPermitRequest)(req, opts);
14
+ const permitResult = await client.createPermitWithMeta(permitRequest);
15
+ const outcome = (0, shared_1.enforceDecision)(req, res, permitResult.permit, opts, permitResult.request_id);
16
+ if (outcome === "allow") {
17
+ next();
18
+ }
19
+ return;
20
+ }
21
+ catch (error) {
22
+ const failure = (0, shared_1.toMiddlewareFailure)(error);
23
+ res.status(failure.httpStatus).json(failure.body);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,10 @@
1
+ import { type KeelMiddlewareOptions, type MethodUrlLike, type MiddlewareResponseLike } from "./shared";
2
+ export interface NextApiRequestLike extends MethodUrlLike {
3
+ [key: string]: unknown;
4
+ }
5
+ export interface NextApiResponseLike extends MiddlewareResponseLike {
6
+ [key: string]: unknown;
7
+ }
8
+ export type NextApiHandler<TReq extends NextApiRequestLike = NextApiRequestLike, TRes extends NextApiResponseLike = NextApiResponseLike> = (req: TReq, res: TRes) => unknown | Promise<unknown>;
9
+ export type KeelNextOptions<TReq extends NextApiRequestLike = NextApiRequestLike, TRes extends NextApiResponseLike = NextApiResponseLike> = KeelMiddlewareOptions<TReq, TRes>;
10
+ export declare function withKeel<TReq extends NextApiRequestLike, TRes extends NextApiResponseLike>(handler: NextApiHandler<TReq, TRes>, opts: KeelNextOptions<TReq, TRes>): NextApiHandler<TReq, TRes>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withKeel = withKeel;
4
+ const shared_1 = require("./shared");
5
+ function withKeel(handler, opts) {
6
+ const client = (0, shared_1.createKeelClientForMiddleware)({
7
+ baseUrl: opts.baseUrl,
8
+ apiKey: opts.apiKey,
9
+ requestFreshness: opts.requestFreshness,
10
+ });
11
+ return async (req, res) => {
12
+ try {
13
+ const permitRequest = (0, shared_1.buildPermitRequest)(req, opts);
14
+ const permitResult = await client.createPermitWithMeta(permitRequest);
15
+ const outcome = (0, shared_1.enforceDecision)(req, res, permitResult.permit, opts, permitResult.request_id);
16
+ if (outcome === "allow") {
17
+ return handler(req, res);
18
+ }
19
+ return;
20
+ }
21
+ catch (error) {
22
+ const failure = (0, shared_1.toMiddlewareFailure)(error);
23
+ return res.status(failure.httpStatus).json(failure.body);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,40 @@
1
+ import { KeelClient, type RequestFreshnessOptions } from "../client";
2
+ import type { Action, PermitRequest, PermitResponse, Resource, Subject } from "../types";
3
+ export interface MiddlewareResponseLike {
4
+ status(code: number): MiddlewareResponseLike;
5
+ json(payload: unknown): unknown;
6
+ }
7
+ export interface KeelMiddlewareOptions<TReq extends MethodUrlLike, TRes extends MiddlewareResponseLike> {
8
+ baseUrl?: string;
9
+ apiKey?: string;
10
+ requestFreshness?: RequestFreshnessOptions;
11
+ projectId: string;
12
+ getSubject(req: TReq): Subject;
13
+ getAction?(req: TReq): Action;
14
+ getResource(req: TReq): Resource;
15
+ getContext?(req: TReq): Record<string, any> | null;
16
+ idempotencyKey?(req: TReq): string;
17
+ onDeny?(req: TReq, res: TRes, permit: PermitResponse): void;
18
+ onChallenge?(req: TReq, res: TRes, permit: PermitResponse): void;
19
+ }
20
+ export interface MethodUrlLike {
21
+ method?: string;
22
+ originalUrl?: string;
23
+ url?: string;
24
+ }
25
+ export interface MiddlewareFailure {
26
+ httpStatus: number;
27
+ body: {
28
+ error: "keel_error";
29
+ message: string;
30
+ status: number;
31
+ };
32
+ }
33
+ export declare function createKeelClientForMiddleware(options: {
34
+ baseUrl?: string;
35
+ apiKey?: string;
36
+ requestFreshness?: RequestFreshnessOptions;
37
+ }): KeelClient;
38
+ export declare function buildPermitRequest<TReq extends MethodUrlLike, TRes extends MiddlewareResponseLike>(req: TReq, options: KeelMiddlewareOptions<TReq, TRes>): PermitRequest;
39
+ export declare function enforceDecision<TReq extends MethodUrlLike, TRes extends MiddlewareResponseLike>(req: TReq, res: TRes, permit: PermitResponse, options: Pick<KeelMiddlewareOptions<TReq, TRes>, "onDeny" | "onChallenge">, responseRequestId?: string): "allow" | "handled";
40
+ export declare function toMiddlewareFailure(error: unknown): MiddlewareFailure;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createKeelClientForMiddleware = createKeelClientForMiddleware;
4
+ exports.buildPermitRequest = buildPermitRequest;
5
+ exports.enforceDecision = enforceDecision;
6
+ exports.toMiddlewareFailure = toMiddlewareFailure;
7
+ const client_1 = require("../client");
8
+ const errors_1 = require("../errors");
9
+ function createKeelClientForMiddleware(options) {
10
+ const baseUrl = options.baseUrl ?? process.env.KEEL_BASE_URL;
11
+ const apiKey = options.apiKey ?? process.env.KEEL_API_KEY;
12
+ if (!baseUrl || !baseUrl.trim()) {
13
+ throw new Error("Keel middleware requires baseUrl or KEEL_BASE_URL.");
14
+ }
15
+ if (!apiKey || !apiKey.trim()) {
16
+ throw new Error("Keel middleware requires apiKey or KEEL_API_KEY.");
17
+ }
18
+ return new client_1.KeelClient({
19
+ baseUrl,
20
+ apiKey,
21
+ requestFreshness: options.requestFreshness,
22
+ });
23
+ }
24
+ function buildPermitRequest(req, options) {
25
+ const subject = options.getSubject(req);
26
+ const action = options.getAction?.(req) ?? {
27
+ name: "request",
28
+ attributes: {},
29
+ };
30
+ const resource = options.getResource(req);
31
+ const context = options.getContext?.(req) ?? null;
32
+ const idempotencyKey = options.idempotencyKey?.(req) ?? defaultIdempotencyKey(req, subject);
33
+ return {
34
+ project_id: options.projectId,
35
+ idempotency_key: idempotencyKey,
36
+ subject,
37
+ action,
38
+ resource,
39
+ context,
40
+ };
41
+ }
42
+ function enforceDecision(req, res, permit, options, responseRequestId) {
43
+ if (permit.decision === "allow") {
44
+ return "allow";
45
+ }
46
+ const requestId = resolveRequestId(permit, responseRequestId);
47
+ if (permit.decision === "deny") {
48
+ if (options.onDeny) {
49
+ options.onDeny(req, res, permit);
50
+ return "handled";
51
+ }
52
+ res.status(403).json({
53
+ error: "deny",
54
+ reason: permit.reason,
55
+ permit,
56
+ ...(requestId ? { request_id: requestId } : {}),
57
+ });
58
+ return "handled";
59
+ }
60
+ if (options.onChallenge) {
61
+ options.onChallenge(req, res, permit);
62
+ return "handled";
63
+ }
64
+ res.status(401).json({
65
+ error: "challenge",
66
+ reason: permit.reason,
67
+ permit,
68
+ ...(requestId ? { request_id: requestId } : {}),
69
+ });
70
+ return "handled";
71
+ }
72
+ function toMiddlewareFailure(error) {
73
+ if (error instanceof errors_1.KeelError) {
74
+ const sourceStatus = Number.isFinite(error.status) && error.status > 0 ? error.status : 502;
75
+ if (sourceStatus >= 400 && sourceStatus < 500) {
76
+ return {
77
+ httpStatus: sourceStatus,
78
+ body: {
79
+ error: "keel_error",
80
+ message: error.message,
81
+ status: sourceStatus,
82
+ },
83
+ };
84
+ }
85
+ return {
86
+ httpStatus: 502,
87
+ body: {
88
+ error: "keel_error",
89
+ message: error.message,
90
+ status: sourceStatus,
91
+ },
92
+ };
93
+ }
94
+ const message = error instanceof Error ? error.message : "Unknown error";
95
+ return {
96
+ httpStatus: 502,
97
+ body: {
98
+ error: "keel_error",
99
+ message,
100
+ status: 502,
101
+ },
102
+ };
103
+ }
104
+ function defaultIdempotencyKey(req, subject) {
105
+ const method = (req.method || "GET").toUpperCase();
106
+ const url = req.originalUrl || req.url || "";
107
+ return `${method}:${url}:${subject.type}:${subject.id}`;
108
+ }
109
+ function resolveRequestId(permit, responseRequestId) {
110
+ if (isNonEmptyString(responseRequestId)) {
111
+ return responseRequestId;
112
+ }
113
+ const requestIdFromPermit = permit
114
+ .request_id;
115
+ if (isNonEmptyString(requestIdFromPermit)) {
116
+ return requestIdFromPermit;
117
+ }
118
+ return undefined;
119
+ }
120
+ function isNonEmptyString(value) {
121
+ return typeof value === "string" && value.trim().length > 0;
122
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExecutionStreamEvent } from "./types";
2
+ export declare function parseSSEStream(response: Response): AsyncGenerator<ExecutionStreamEvent>;