@graphoria/server 0.3.0 → 0.4.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 +24 -20
- package/dist/src/ai/mcp/index.d.ts +9 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/authentication/capabilities.d.ts +18 -0
- package/dist/src/authentication/capabilities.d.ts.map +1 -0
- package/dist/src/authentication/capabilities.test.d.ts +2 -0
- package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/secrets.d.ts +9 -0
- package/dist/src/authentication/secrets.d.ts.map +1 -0
- package/dist/src/authentication/secrets.test.d.ts +2 -0
- package/dist/src/authentication/secrets.test.d.ts.map +1 -0
- package/dist/src/configuration/getSchemas/index.d.ts +0 -2
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +0 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +0 -1
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts +2 -0
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +12 -4
- package/dist/src/console/session.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +1 -1
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +1 -1
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
- package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
- package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/audit.d.ts +27 -0
- package/dist/src/logging/audit.d.ts.map +1 -0
- package/dist/src/logging/audit.test.d.ts +2 -0
- package/dist/src/logging/audit.test.d.ts.map +1 -0
- package/dist/src/singletons/env.d.ts +8 -4
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/types/env.d.ts +24 -12
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/utils/responses.d.ts +3 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +2 -0
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/playgrounds/console/index.html +7 -7
- package/playgrounds/graphiql/index.html +548 -685
- package/src/ai/mcp/index.ts +19 -14
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
- package/src/authentication/capabilities.ts +54 -0
- package/src/authentication/index.ts +3 -3
- package/src/authentication/jwt.ts +30 -25
- package/src/authentication/paseto.ts +36 -26
- package/src/authentication/secrets.ts +30 -0
- package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
- package/src/configuration/gql/gqlAuthOperations.ts +26 -0
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +17 -5
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
- package/src/configuration/rest/generateOpenAPI.ts +61 -11
- package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
- package/src/console/api.ts +63 -18
- package/src/console/session.ts +30 -26
- package/src/databases/core/executor.ts +1 -2
- package/src/databases/core/function-mapping.ts +0 -1
- package/src/databases/core/procedure-arguments.ts +25 -0
- package/src/databases/engines/mssql/connection.ts +0 -3
- package/src/databases/engines/mysql/connection.ts +10 -7
- package/src/databases/engines/postgresql/connection.ts +10 -7
- package/src/index.ts +51 -12
- package/src/logging/audit.ts +86 -0
- package/src/types/env.ts +26 -8
- package/src/utils/responses.ts +6 -0
- package/src/utils/sessionVariables.ts +2 -0
package/src/index.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { serve } from "bun";
|
|
|
4
4
|
import { isString } from "es-toolkit";
|
|
5
5
|
|
|
6
6
|
import type { BunRequest } from "bun";
|
|
7
|
+
import type { Capability } from "./authentication/capabilities";
|
|
7
8
|
import type { Configuration } from "./types/configuration";
|
|
8
9
|
import type { Env } from "./types/env";
|
|
10
|
+
import type { SessionContext } from "./utils/sessionVariables";
|
|
9
11
|
|
|
10
12
|
import { createTokenService } from "./authentication";
|
|
11
13
|
import { analyzeConfiguration, loadConfiguration } from "./configuration";
|
|
@@ -14,6 +16,7 @@ import { websocketHandlerFactory } from "./configuration/gql/handleGraphQLSubscr
|
|
|
14
16
|
import { consoleRoutesFactory } from "./console/api";
|
|
15
17
|
import { createAuthTables, verifyAuthTablesExist } from "./databases";
|
|
16
18
|
import { createMCPRoutes } from "./ai";
|
|
19
|
+
import { createCapabilityAuthorizer } from "./authentication/capabilities";
|
|
17
20
|
import { getAgent, instantiateAI } from "./singletons/ai";
|
|
18
21
|
import { getTokenService, setTokenService } from "./singletons/authentication";
|
|
19
22
|
import { instantiateCronJobs } from "./singletons/cron";
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
import { S200, S400, S401, S404, S429 } from "./utils/responses";
|
|
33
36
|
import { writeSchema } from "./utils/writeSchema";
|
|
34
37
|
import { logger, configureLogging } from "./logging";
|
|
38
|
+
import { actorFromSession, audit } from "./logging/audit";
|
|
35
39
|
|
|
36
40
|
// Re-export for consumers
|
|
37
41
|
export { configureLogging };
|
|
@@ -244,24 +248,52 @@ const createGraphQLServer = async (env: Env) => {
|
|
|
244
248
|
: createMemoryRateLimitStore(),
|
|
245
249
|
});
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
const authorizeCapability = createCapabilityAuthorizer(env);
|
|
252
|
+
|
|
253
|
+
// Helper to get role-based handlers. A route that names a capability also
|
|
254
|
+
// accepts that capability's scoped credential in the admin-secret header;
|
|
255
|
+
// it stands in for the superadmin role on that route and nowhere else.
|
|
256
|
+
const getRoleHandlers = async (
|
|
257
|
+
req: Request,
|
|
258
|
+
server?: Bun.Server<unknown>,
|
|
259
|
+
capability?: Capability,
|
|
260
|
+
) => {
|
|
261
|
+
const adminSecretHeader = req.headers.get(env.admin.header);
|
|
262
|
+
const grant = capability ? authorizeCapability(adminSecretHeader, capability) : null;
|
|
263
|
+
|
|
264
|
+
const session: SessionContext =
|
|
265
|
+
grant && !grant.superset
|
|
266
|
+
? { sub: capability, role: env.superadmin.role, authMethod: "admin_secret" }
|
|
267
|
+
: await getTokenService().verifyTokenAndGetSession(
|
|
268
|
+
req.headers.get(env.authorizationHeader),
|
|
269
|
+
adminSecretHeader,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const ip = resolveClientAddress(req, server, env.rateLimit.trustProxy);
|
|
253
273
|
|
|
254
274
|
// The limit needs the role, and the role costs a token verification plus a
|
|
255
275
|
// revocation lookup — so it is spent here, once, rather than again inside a
|
|
256
276
|
// wrapper.
|
|
257
|
-
const limit = await rateLimiter?.check(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
277
|
+
const limit = await rateLimiter?.check(session, ip);
|
|
278
|
+
|
|
279
|
+
const scope: Capability | "all" | undefined = grant
|
|
280
|
+
? grant.superset
|
|
281
|
+
? "all"
|
|
282
|
+
: capability
|
|
283
|
+
: undefined;
|
|
284
|
+
|
|
285
|
+
if (session.authMethod === "admin_secret") {
|
|
286
|
+
audit().emit({
|
|
287
|
+
action: "admin_secret.used",
|
|
288
|
+
actor: { type: "admin_secret", ...(scope ? { scope } : {}), ip },
|
|
289
|
+
target: { kind: "endpoint", method: req.method, path: new URL(req.url).pathname },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
261
292
|
|
|
262
293
|
return {
|
|
263
294
|
role: session.role!,
|
|
264
295
|
session,
|
|
296
|
+
scope,
|
|
265
297
|
limit,
|
|
266
298
|
...analyzedConfiguration.roles[session.role!].handlers,
|
|
267
299
|
};
|
|
@@ -380,7 +412,7 @@ const createGraphQLServer = async (env: Env) => {
|
|
|
380
412
|
name: projectConfiguration.name,
|
|
381
413
|
version: projectConfiguration.version,
|
|
382
414
|
maxQueryDepth: env.ai?.mcp?.maxQueryDepth ?? env.maxQueryDepth,
|
|
383
|
-
|
|
415
|
+
authorize: authorizeCapability,
|
|
384
416
|
adminSecretHeader: env.admin.header,
|
|
385
417
|
});
|
|
386
418
|
|
|
@@ -395,7 +427,7 @@ const createGraphQLServer = async (env: Env) => {
|
|
|
395
427
|
...(env.enableCors ? { OPTIONS: () => new S200(null) } : {}),
|
|
396
428
|
POST: async (req: BunRequest, server: Bun.Server<unknown>) => {
|
|
397
429
|
try {
|
|
398
|
-
const { role, limit } = await getRoleHandlers(req, server);
|
|
430
|
+
const { role, session, scope, limit } = await getRoleHandlers(req, server, "ai");
|
|
399
431
|
if (limit && !limit.allowed) return new S429(limit.retryAfterMs);
|
|
400
432
|
if (role !== env.superadmin.role) return new S404({ error: "Not Found" });
|
|
401
433
|
|
|
@@ -405,6 +437,13 @@ const createGraphQLServer = async (env: Env) => {
|
|
|
405
437
|
errors: [{ message: "`prompt` (string) is required" }],
|
|
406
438
|
});
|
|
407
439
|
|
|
440
|
+
audit().emit({
|
|
441
|
+
action: "ai.ask",
|
|
442
|
+
actor: { ...actorFromSession(session), ...(scope ? { scope } : {}) },
|
|
443
|
+
target: { kind: "ai", via: "rest" },
|
|
444
|
+
prompt,
|
|
445
|
+
});
|
|
446
|
+
|
|
408
447
|
return new S200({ answer: await getAgent()(prompt) });
|
|
409
448
|
} catch (error) {
|
|
410
449
|
return new S400({ errors: [{ message: (error as Error)?.message }] });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { Capability } from "../authentication/capabilities";
|
|
3
|
+
import type { SessionContext } from "../utils/sessionVariables";
|
|
4
|
+
|
|
5
|
+
import { logger } from "./index";
|
|
6
|
+
|
|
7
|
+
export type AuditActor = {
|
|
8
|
+
type: "admin_secret" | "console" | "token" | "credentials" | "anonymous";
|
|
9
|
+
sub?: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
/** Which credential was presented at a scoped surface: `all` is the admin secret. */
|
|
12
|
+
scope?: Capability | "all";
|
|
13
|
+
ip?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AuditEvent = {
|
|
17
|
+
action: string;
|
|
18
|
+
actor: AuditActor;
|
|
19
|
+
target: Record<string, unknown>;
|
|
20
|
+
outcome?: "success" | "failure";
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AuditLog = {
|
|
25
|
+
emit(event: AuditEvent): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const REDACTED_KEYS = new Set([
|
|
29
|
+
"password",
|
|
30
|
+
"secret",
|
|
31
|
+
"secrets",
|
|
32
|
+
"token",
|
|
33
|
+
"access_token",
|
|
34
|
+
"refresh_token",
|
|
35
|
+
"authorization",
|
|
36
|
+
"cookie",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const redact = (value: unknown): unknown => {
|
|
40
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
41
|
+
if (typeof value !== "object" || value === null) return value;
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(value).map(([key, entry]) => [
|
|
44
|
+
key,
|
|
45
|
+
REDACTED_KEYS.has(key) ? "[REDACTED]" : redact(entry),
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const actorFromSession = (session: SessionContext | undefined): AuditActor => {
|
|
51
|
+
if (!session) return { type: "anonymous" };
|
|
52
|
+
const type =
|
|
53
|
+
session.authMethod === "admin_secret" ? "admin_secret" : session.jti ? "token" : "anonymous";
|
|
54
|
+
return {
|
|
55
|
+
type,
|
|
56
|
+
...(session.sub !== undefined ? { sub: session.sub } : {}),
|
|
57
|
+
...(session.role !== undefined ? { role: session.role } : {}),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const createAuditLog = (base: Logger): AuditLog => {
|
|
62
|
+
// An audit record has to survive LOG_LEVEL=warn: the level is pinned here,
|
|
63
|
+
// independent of whatever the root logger was configured with.
|
|
64
|
+
const log = base.child({});
|
|
65
|
+
log.level = "info";
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
emit: (event) => {
|
|
69
|
+
log.info(redact(event) as object, event.action);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let override: AuditLog | null = null;
|
|
75
|
+
let instance: AuditLog | null = null;
|
|
76
|
+
|
|
77
|
+
/** Test seam: pass `null` to restore the default log. */
|
|
78
|
+
export const setAuditLog = (log: AuditLog | null): void => {
|
|
79
|
+
override = log;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const audit = (): AuditLog => {
|
|
83
|
+
if (override) return override;
|
|
84
|
+
if (!instance) instance = createAuditLog(logger("audit"));
|
|
85
|
+
return instance;
|
|
86
|
+
};
|
package/src/types/env.ts
CHANGED
|
@@ -3,19 +3,29 @@ import { z } from "zod";
|
|
|
3
3
|
import { version } from "../../package.json";
|
|
4
4
|
import { ConfigurationZod } from "./zod/configuration";
|
|
5
5
|
|
|
6
|
+
// A secret env var is a comma-separated list: the first entry is the one in
|
|
7
|
+
// use, the rest are still accepted so a rotation can overlap instead of cutting
|
|
8
|
+
// over. Blank entries are dropped, so an unset var is an empty list.
|
|
9
|
+
const splitSecrets = (value: string) =>
|
|
10
|
+
value
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((entry) => entry.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
const secretList = z.string().default("").transform(splitSecrets);
|
|
15
|
+
|
|
6
16
|
export const EnvZod = z
|
|
7
17
|
.object({
|
|
8
18
|
PORT: z.coerce.number().default(3000),
|
|
9
19
|
NODE_ENV: z.string().default("DEVELOPMENT"),
|
|
10
|
-
ADMIN_SECRET: z.string(),
|
|
20
|
+
ADMIN_SECRET: z.string().transform(splitSecrets),
|
|
11
21
|
CONFIGURATION: z.union([z.string(), ConfigurationZod]).optional(),
|
|
12
22
|
ANONYMOUS_ROLE: z.string().default("anonymous"),
|
|
13
|
-
JWT_SECRET:
|
|
23
|
+
JWT_SECRET: secretList,
|
|
14
24
|
JWT_EXPIRES_IN: z.string().default("5m"),
|
|
15
25
|
JWT_RT_EXPIRES_IN: z.string().default("7d"),
|
|
16
|
-
PASETO_LOCAL_KEY:
|
|
26
|
+
PASETO_LOCAL_KEY: secretList,
|
|
17
27
|
PASETO_SECRET_KEY: z.string().optional().default(""),
|
|
18
|
-
PASETO_PUBLIC_KEY:
|
|
28
|
+
PASETO_PUBLIC_KEY: secretList,
|
|
19
29
|
AUTH_STRATEGY: z.enum(["jwt", "paseto_local", "paseto_public"]).optional(),
|
|
20
30
|
REDIS_URL: z.string().default("redis://localhost:6379"),
|
|
21
31
|
CACHE_STORE: z.enum(["memory", "redis"]).default("memory"),
|
|
@@ -31,6 +41,8 @@ export const EnvZod = z
|
|
|
31
41
|
CONSOLE_ENABLED: z.stringbool().default(false),
|
|
32
42
|
CONSOLE_ENDPOINT: z.string().default("/_console"),
|
|
33
43
|
CONSOLE_SESSION_EXPIRES_IN: z.string().default("1h"),
|
|
44
|
+
CONSOLE_READ_SECRET: secretList,
|
|
45
|
+
CONSOLE_WRITE_SECRET: secretList,
|
|
34
46
|
PREFIX: z.string().default(""),
|
|
35
47
|
CORS_ENABLED: z.stringbool().default(true),
|
|
36
48
|
PRINT_SCHEMAS: z.stringbool().default(false),
|
|
@@ -48,9 +60,11 @@ export const EnvZod = z
|
|
|
48
60
|
AI_ENABLED: z.stringbool().optional(),
|
|
49
61
|
AI_GRAPHQL_ENABLED: z.stringbool().default(true),
|
|
50
62
|
AI_REST_ENABLED: z.stringbool().default(true),
|
|
63
|
+
AI_SECRET: secretList,
|
|
51
64
|
AI_MCP_ENABLED: z.stringbool().optional(),
|
|
52
65
|
AI_MCP_ENDPOINT: z.string().default("/mcp"),
|
|
53
66
|
AI_MCP_REQUIRE_ADMIN_SECRET: z.stringbool().default(false),
|
|
67
|
+
AI_MCP_SECRET: secretList,
|
|
54
68
|
AI_MCP_MAX_QUERY_DEPTH: z.coerce.number().int().min(0).optional(),
|
|
55
69
|
AI_MCP_DISABLED_TOOLS: z.string().default(""),
|
|
56
70
|
AI_MCP_DISABLED_RESOURCES: z.string().default(""),
|
|
@@ -80,18 +94,18 @@ export const EnvZod = z
|
|
|
80
94
|
queryTimeoutMs: env.QUERY_TIMEOUT_MS,
|
|
81
95
|
maxQueryCost: env.MAX_QUERY_COST,
|
|
82
96
|
admin: {
|
|
83
|
-
|
|
97
|
+
secrets: env.ADMIN_SECRET,
|
|
84
98
|
header: env.ADMIN_SECRET_HEADER,
|
|
85
99
|
},
|
|
86
100
|
jwt: {
|
|
87
|
-
|
|
101
|
+
secrets: env.JWT_SECRET,
|
|
88
102
|
expiresIn: env.JWT_EXPIRES_IN,
|
|
89
103
|
rtExpiresIn: env.JWT_RT_EXPIRES_IN,
|
|
90
104
|
},
|
|
91
105
|
paseto: {
|
|
92
|
-
|
|
106
|
+
localKeys: env.PASETO_LOCAL_KEY,
|
|
93
107
|
secretKey: env.PASETO_SECRET_KEY,
|
|
94
|
-
|
|
108
|
+
publicKeys: env.PASETO_PUBLIC_KEY,
|
|
95
109
|
},
|
|
96
110
|
rateLimit: {
|
|
97
111
|
max: env.RATE_LIMIT_MAX,
|
|
@@ -114,15 +128,19 @@ export const EnvZod = z
|
|
|
114
128
|
enabled: env.CONSOLE_ENABLED,
|
|
115
129
|
endpoint: env.CONSOLE_ENDPOINT,
|
|
116
130
|
sessionExpiresIn: env.CONSOLE_SESSION_EXPIRES_IN,
|
|
131
|
+
readSecrets: env.CONSOLE_READ_SECRET,
|
|
132
|
+
writeSecrets: env.CONSOLE_WRITE_SECRET,
|
|
117
133
|
},
|
|
118
134
|
ai: {
|
|
119
135
|
enabled: env.AI_ENABLED,
|
|
120
136
|
graphqlEnabled: env.AI_GRAPHQL_ENABLED,
|
|
121
137
|
restEnabled: env.AI_REST_ENABLED,
|
|
138
|
+
secrets: env.AI_SECRET,
|
|
122
139
|
mcp: {
|
|
123
140
|
enabled: env.AI_MCP_ENABLED,
|
|
124
141
|
endpoint: env.AI_MCP_ENDPOINT,
|
|
125
142
|
requireAdminSecret: env.AI_MCP_REQUIRE_ADMIN_SECRET,
|
|
143
|
+
secrets: env.AI_MCP_SECRET,
|
|
126
144
|
maxQueryDepth: env.AI_MCP_MAX_QUERY_DEPTH,
|
|
127
145
|
disabledTools: env.AI_MCP_DISABLED_TOOLS
|
|
128
146
|
? env.AI_MCP_DISABLED_TOOLS.split(",")
|
package/src/utils/responses.ts
CHANGED
|
@@ -29,6 +29,12 @@ export class S401 extends ClientResponse {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export class S403 extends ClientResponse {
|
|
33
|
+
constructor(body?: object | null, init?: ResponseInit) {
|
|
34
|
+
super(body, { ...init, status: 403 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export class S404 extends ClientResponse {
|
|
33
39
|
constructor(body?: object | null, init?: ResponseInit) {
|
|
34
40
|
super(body, { ...init, status: 404 });
|