@kozou/api 0.0.1 → 1.0.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/LICENSE +202 -0
- package/README.md +200 -1
- package/dist/auth.d.ts +75 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +128 -0
- package/dist/auth.js.map +1 -0
- package/dist/embed.d.ts +46 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +181 -0
- package/dist/embed.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +39 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +267 -0
- package/dist/handler.js.map +1 -0
- package/dist/ident.d.ts +7 -0
- package/dist/ident.d.ts.map +1 -0
- package/dist/ident.js +12 -0
- package/dist/ident.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.d.ts +9 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +300 -0
- package/dist/openapi.js.map +1 -0
- package/dist/query-builder.d.ts +83 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +592 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/schema-lookup.d.ts +30 -0
- package/dist/schema-lookup.d.ts.map +1 -0
- package/dist/schema-lookup.js +61 -0
- package/dist/schema-lookup.js.map +1 -0
- package/dist/startApiServer.d.ts +53 -0
- package/dist/startApiServer.d.ts.map +1 -0
- package/dist/startApiServer.js +210 -0
- package/dist/startApiServer.js.map +1 -0
- package/package.json +44 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Resolve a request path segment (e.g. "books" or "public.books") to a
|
|
2
|
+
// concrete table or view from the introspected SchemaContext. This map is
|
|
3
|
+
// the allowlist: only resources that exist in the schema are addressable,
|
|
4
|
+
// and only their declared columns can be filtered / sorted on.
|
|
5
|
+
export function buildResourceLookup(schema) {
|
|
6
|
+
const resources = [];
|
|
7
|
+
for (const t of schema.tables) {
|
|
8
|
+
resources.push({
|
|
9
|
+
kind: 'table',
|
|
10
|
+
schema: t.schema,
|
|
11
|
+
name: t.name,
|
|
12
|
+
qualifiedName: t.qualifiedName,
|
|
13
|
+
columns: t.columns,
|
|
14
|
+
primaryKey: t.primaryKey,
|
|
15
|
+
relations: t.relations ?? [],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
for (const v of schema.views) {
|
|
19
|
+
resources.push({
|
|
20
|
+
kind: 'view',
|
|
21
|
+
schema: v.schema,
|
|
22
|
+
name: v.name,
|
|
23
|
+
qualifiedName: v.qualifiedName,
|
|
24
|
+
columns: v.columns,
|
|
25
|
+
primaryKey: [],
|
|
26
|
+
relations: [],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const byKey = new Map();
|
|
30
|
+
const bareNameCounts = new Map();
|
|
31
|
+
for (const r of resources) {
|
|
32
|
+
byKey.set(r.qualifiedName, r);
|
|
33
|
+
bareNameCounts.set(r.name, (bareNameCounts.get(r.name) ?? 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
// Register the bare name only when it is unique across schemas and does
|
|
36
|
+
// not collide with an existing qualified-name key.
|
|
37
|
+
for (const r of resources) {
|
|
38
|
+
if (bareNameCounts.get(r.name) === 1 && !byKey.has(r.name)) {
|
|
39
|
+
byKey.set(r.name, r);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const sortedNames = resources.map((r) => r.qualifiedName).sort();
|
|
43
|
+
// Reverse index: parent qualifiedName -> children that reference it.
|
|
44
|
+
const reverseIndex = new Map();
|
|
45
|
+
for (const r of resources) {
|
|
46
|
+
for (const rel of r.relations) {
|
|
47
|
+
const parentKey = `${rel.references.schema}.${rel.references.table}`;
|
|
48
|
+
const list = reverseIndex.get(parentKey);
|
|
49
|
+
if (list)
|
|
50
|
+
list.push({ child: r, relation: rel });
|
|
51
|
+
else
|
|
52
|
+
reverseIndex.set(parentKey, [{ child: r, relation: rel }]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
resolve: (name) => byKey.get(name),
|
|
57
|
+
list: () => sortedNames,
|
|
58
|
+
reverse: (qualifiedName) => reverseIndex.get(qualifiedName) ?? [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=schema-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-lookup.js","sourceRoot":"","sources":["../src/schema-lookup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import type { SchemaContext } from '@kozou/core';
|
|
3
|
+
import { type ApiHandlerDeps, type Queryable } from './handler.js';
|
|
4
|
+
import { type AuthConfig } from './auth.js';
|
|
5
|
+
/** A pooled client: a Queryable that can be returned to its pool. A
|
|
6
|
+
* node-postgres `PoolClient` satisfies this. */
|
|
7
|
+
export type PoolClient = Queryable & {
|
|
8
|
+
release(err?: boolean | Error): void;
|
|
9
|
+
};
|
|
10
|
+
/** A connection pool able to hand out dedicated clients. A `pg.Pool` fits. */
|
|
11
|
+
export type ConnectionPool = {
|
|
12
|
+
connect(): Promise<PoolClient>;
|
|
13
|
+
};
|
|
14
|
+
export type StartApiServerOptions = {
|
|
15
|
+
/** Introspected schema that drives routing + the identifier allowlist. */
|
|
16
|
+
schema: SchemaContext;
|
|
17
|
+
/** Open connection (a `pg.Pool` is the expected caller-owned value). */
|
|
18
|
+
db: Queryable;
|
|
19
|
+
/** Required when `auth` is set: source of dedicated clients for the
|
|
20
|
+
* per-request transaction that carries the role + claims. A `pg.Pool` fits. */
|
|
21
|
+
pool?: ConnectionPool;
|
|
22
|
+
/** Opt-in JWT verification + RLS enforcement. Omit for zero-auth behavior. */
|
|
23
|
+
auth?: AuthConfig;
|
|
24
|
+
/** TCP port to listen on. Default: 3335 (3333 = UI, 3334 = MCP HTTP). */
|
|
25
|
+
port?: number;
|
|
26
|
+
/** Host/interface to bind. Default: 127.0.0.1 (loopback only). */
|
|
27
|
+
host?: string;
|
|
28
|
+
/** Version string advertised in `GET /`. */
|
|
29
|
+
version?: string;
|
|
30
|
+
/** Prefix used in stderr log lines. Default: '[@kozou/api]'. */
|
|
31
|
+
logPrefix?: string;
|
|
32
|
+
};
|
|
33
|
+
export type ApiServerHandle = {
|
|
34
|
+
/** The actual bound port (resolves an ephemeral `0` to the real port). */
|
|
35
|
+
port: number;
|
|
36
|
+
/** The host the server is bound to. */
|
|
37
|
+
host: string;
|
|
38
|
+
/** Stop accepting connections and resolve once the server has closed. */
|
|
39
|
+
close: () => Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Build a node:http request listener over the framework-agnostic handler.
|
|
44
|
+
* Exposed separately so it can be mounted by an embedding server or driven
|
|
45
|
+
* directly in tests.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createApiRequestListener(deps: ApiHandlerDeps): (req: IncomingMessage, res: ServerResponse) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Start the REST API over node:http, bound to the given schema + db.
|
|
50
|
+
* Resolves once the server is listening.
|
|
51
|
+
*/
|
|
52
|
+
export declare function startApiServer(opts: StartApiServerOptions): Promise<ApiServerHandle>;
|
|
53
|
+
//# sourceMappingURL=startApiServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startApiServer.d.ts","sourceRoot":"","sources":["../src/startApiServer.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,cAAc,EACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB,OAAO,EAAuB,KAAK,UAAU,EAAsB,MAAM,WAAW,CAAC;AAErF;iDACiD;AACjD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAA;CAAE,CAAC;AAE9E,8EAA8E;AAC9E,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CAAE,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,0EAA0E;IAC1E,MAAM,EAAE,aAAa,CAAC;IACtB,wEAAwE;IACxE,EAAE,EAAE,SAAS,CAAC;IACd;oFACgF;IAChF,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAOF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAkBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,cAAc,GACnB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAOrD;AAkHD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmD1F"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// node:http wiring for the Kozou REST layer. Mirrors @kozou/mcp's
|
|
2
|
+
// startHttpServer: a thin request listener over the framework-agnostic
|
|
3
|
+
// handler, bound to loopback by default with a loud warning when bound to
|
|
4
|
+
// a non-loopback host (Kozou v0.1 spec §18.5 — the v0.2 API has no auth).
|
|
5
|
+
import { createServer, } from 'node:http';
|
|
6
|
+
import { errorBody, KozouApiError } from './errors.js';
|
|
7
|
+
import { handleApiRequest, } from './handler.js';
|
|
8
|
+
import { buildResourceLookup } from './schema-lookup.js';
|
|
9
|
+
import { buildOpenApiDocument } from './openapi.js';
|
|
10
|
+
import { quoteIdent } from './ident.js';
|
|
11
|
+
import { createAuthenticator } from './auth.js';
|
|
12
|
+
const DEFAULT_PORT = 3335;
|
|
13
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
14
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']);
|
|
15
|
+
export function isLoopbackHost(host) {
|
|
16
|
+
return LOOPBACK_HOSTS.has(host);
|
|
17
|
+
}
|
|
18
|
+
function nonLoopbackWarning(host, prefix, authed) {
|
|
19
|
+
if (authed) {
|
|
20
|
+
return (`${prefix} NOTE: REST API bound to non-loopback host "${host}".\n` +
|
|
21
|
+
`${prefix} JWT auth is enabled; terminate TLS in front of it so tokens\n` +
|
|
22
|
+
`${prefix} are never sent in clear text.\n`);
|
|
23
|
+
}
|
|
24
|
+
return (`${prefix} WARNING: REST API bound to non-loopback host "${host}".\n` +
|
|
25
|
+
`${prefix} This API has NO authentication configured (Kozou v0.1 spec §18.5).\n` +
|
|
26
|
+
`${prefix} Anyone who can reach ${host} can read and write this database.\n` +
|
|
27
|
+
`${prefix} Bind to 127.0.0.1 (the default) or configure JWT auth.\n`);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a node:http request listener over the framework-agnostic handler.
|
|
31
|
+
* Exposed separately so it can be mounted by an embedding server or driven
|
|
32
|
+
* directly in tests.
|
|
33
|
+
*/
|
|
34
|
+
export function createApiRequestListener(deps) {
|
|
35
|
+
return (req, res) => {
|
|
36
|
+
dispatch(deps, req, res).catch((err) => {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
respondJson(res, 500, errorBody('internal', message));
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function buildHttpRequest(req) {
|
|
43
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
44
|
+
const segments = url.pathname
|
|
45
|
+
.split('/')
|
|
46
|
+
.filter((s) => s.length > 0)
|
|
47
|
+
.map((s) => safeDecode(s));
|
|
48
|
+
const body = await readJsonBody(req);
|
|
49
|
+
return {
|
|
50
|
+
method: req.method ?? 'GET',
|
|
51
|
+
segments,
|
|
52
|
+
query: url.searchParams,
|
|
53
|
+
body,
|
|
54
|
+
headers: req.headers,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function dispatch(deps, req, res) {
|
|
58
|
+
const result = await handleApiRequest(deps, await buildHttpRequest(req));
|
|
59
|
+
respondJson(res, result.status, result.body);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Authenticated dispatch: verify the JWT, then run the request inside a
|
|
63
|
+
* transaction on a dedicated client under `SET LOCAL ROLE` with the claims
|
|
64
|
+
* published so the database's row-level-security policies apply.
|
|
65
|
+
*/
|
|
66
|
+
async function dispatchAuthed(base, authenticator, pool, req, res) {
|
|
67
|
+
let auth;
|
|
68
|
+
try {
|
|
69
|
+
auth = await authenticator.authenticate(singleHeader(req.headers, 'authorization'));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Reject before acquiring a connection (no leak on 401 / 403).
|
|
73
|
+
respondError(res, err);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const httpReq = await buildHttpRequest(req);
|
|
77
|
+
const client = await pool.connect();
|
|
78
|
+
try {
|
|
79
|
+
await client.query('BEGIN');
|
|
80
|
+
try {
|
|
81
|
+
// Role is an identifier (no bound-parameter form); quote it. The role is
|
|
82
|
+
// additionally constrained by the auth allowlist.
|
|
83
|
+
await client.query(`SET LOCAL ROLE ${quoteIdent(auth.role)}`);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error('Could not assume the requested role.');
|
|
87
|
+
}
|
|
88
|
+
// Claims are a value: bound parameter, never interpolated into SQL.
|
|
89
|
+
await client.query('SELECT set_config($1, $2, true)', [
|
|
90
|
+
authenticator.claimsGuc,
|
|
91
|
+
JSON.stringify(auth.claims),
|
|
92
|
+
]);
|
|
93
|
+
// The client is a Queryable; routing runs every query on it, inside this
|
|
94
|
+
// transaction, so the role + claims apply.
|
|
95
|
+
const deps = { ...base, db: client };
|
|
96
|
+
const result = await handleApiRequest(deps, httpReq);
|
|
97
|
+
await client.query('COMMIT');
|
|
98
|
+
respondJson(res, result.status, result.body);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
try {
|
|
102
|
+
await client.query('ROLLBACK');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// The connection may already be in a failed state; nothing to do.
|
|
106
|
+
}
|
|
107
|
+
respondError(res, err);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
client.release();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function singleHeader(headers, name) {
|
|
114
|
+
const value = headers[name];
|
|
115
|
+
return Array.isArray(value) ? value[0] : value;
|
|
116
|
+
}
|
|
117
|
+
function respondError(res, err) {
|
|
118
|
+
if (err instanceof KozouApiError) {
|
|
119
|
+
respondJson(res, err.status, errorBody(err.code, err.message));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Never expose internal error detail (which can carry stack or database
|
|
123
|
+
// information) to the client: log it server-side, return a generic message.
|
|
124
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
125
|
+
process.stderr.write(`[@kozou/api] request failed: ${detail}\n`);
|
|
126
|
+
respondJson(res, 500, errorBody('internal', 'Internal server error.'));
|
|
127
|
+
}
|
|
128
|
+
async function readJsonBody(req) {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
for await (const chunk of req) {
|
|
131
|
+
chunks.push(chunk);
|
|
132
|
+
}
|
|
133
|
+
if (chunks.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
136
|
+
if (raw.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Start the REST API over node:http, bound to the given schema + db.
|
|
147
|
+
* Resolves once the server is listening.
|
|
148
|
+
*/
|
|
149
|
+
export async function startApiServer(opts) {
|
|
150
|
+
const prefix = opts.logPrefix ?? '[@kozou/api]';
|
|
151
|
+
const host = opts.host ?? DEFAULT_HOST;
|
|
152
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
153
|
+
const authenticator = opts.auth ? createAuthenticator(opts.auth) : undefined;
|
|
154
|
+
if (authenticator !== undefined && opts.pool === undefined) {
|
|
155
|
+
throw new Error(`${prefix} auth requires a "pool" (e.g. a pg.Pool) to run each request under SET LOCAL ROLE.`);
|
|
156
|
+
}
|
|
157
|
+
if (!isLoopbackHost(host)) {
|
|
158
|
+
process.stderr.write(nonLoopbackWarning(host, prefix, authenticator !== undefined));
|
|
159
|
+
}
|
|
160
|
+
const base = {
|
|
161
|
+
db: opts.db,
|
|
162
|
+
lookup: buildResourceLookup(opts.schema),
|
|
163
|
+
version: opts.version,
|
|
164
|
+
openapi: buildOpenApiDocument(opts.schema, { version: opts.version }),
|
|
165
|
+
};
|
|
166
|
+
const pool = opts.pool;
|
|
167
|
+
const listener = authenticator !== undefined && pool !== undefined
|
|
168
|
+
? (req, res) => {
|
|
169
|
+
dispatchAuthed(base, authenticator, pool, req, res).catch((err) => respondError(res, err));
|
|
170
|
+
}
|
|
171
|
+
: createApiRequestListener(base);
|
|
172
|
+
const httpServer = createServer(listener);
|
|
173
|
+
await new Promise((resolve, reject) => {
|
|
174
|
+
const onError = (err) => reject(err);
|
|
175
|
+
httpServer.once('error', onError);
|
|
176
|
+
httpServer.listen(port, host, () => {
|
|
177
|
+
httpServer.removeListener('error', onError);
|
|
178
|
+
resolve();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
const boundPort = httpServer.address().port;
|
|
182
|
+
process.stderr.write(`${prefix} REST API listening on http://${host}:${boundPort}\n`);
|
|
183
|
+
return {
|
|
184
|
+
port: boundPort,
|
|
185
|
+
host,
|
|
186
|
+
close: () => closeServer(httpServer),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function safeDecode(segment) {
|
|
190
|
+
try {
|
|
191
|
+
return decodeURIComponent(segment);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return segment;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function respondJson(res, status, body) {
|
|
198
|
+
if (res.headersSent) {
|
|
199
|
+
res.end();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
203
|
+
res.end(JSON.stringify(body));
|
|
204
|
+
}
|
|
205
|
+
function closeServer(httpServer) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
httpServer.close((err) => (err ? reject(err) : resolve()));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=startApiServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startApiServer.js","sourceRoot":"","sources":["../src/startApiServer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kozou/api",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Kozou's own REST layer: serves table CRUD / VIEW reads / OpenAPI from a SchemaContext.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kozou-dev/kozou.git",
|
|
9
|
+
"directory": "packages/api"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://kozou.org",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kozou-dev/kozou/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"jose": "^6.2.3",
|
|
34
|
+
"pg": "^8.13.0",
|
|
35
|
+
"@kozou/core": "1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/pg": "^8.11.0",
|
|
39
|
+
"@kozou/test-utils": "0.0.0",
|
|
40
|
+
"@kozou/introspect": "1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"test": "vitest run --coverage"
|
|
46
|
+
}
|
|
47
|
+
}
|