@nacre.work/api 0.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/README.md +3 -0
- package/dist/__tests__/admin-surface.test.d.ts +2 -0
- package/dist/__tests__/admin-surface.test.d.ts.map +1 -0
- package/dist/__tests__/admin-surface.test.js +431 -0
- package/dist/__tests__/admin-surface.test.js.map +1 -0
- package/dist/__tests__/init.test.d.ts +2 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/__tests__/init.test.js +125 -0
- package/dist/__tests__/init.test.js.map +1 -0
- package/dist/__tests__/service-keys.test.d.ts +2 -0
- package/dist/__tests__/service-keys.test.d.ts.map +1 -0
- package/dist/__tests__/service-keys.test.js +204 -0
- package/dist/__tests__/service-keys.test.js.map +1 -0
- package/dist/adapters.d.ts +197 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +672 -0
- package/dist/adapters.js.map +1 -0
- package/dist/auth.d.ts +42 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +106 -0
- package/dist/auth.js.map +1 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +101 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +49 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +201 -0
- package/dist/init.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +113 -0
- package/dist/main.js.map +1 -0
- package/dist/server.d.ts +239 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +543 -0
- package/dist/server.js.map +1 -0
- package/dist/service-keys.d.ts +82 -0
- package/dist/service-keys.d.ts.map +1 -0
- package/dist/service-keys.js +158 -0
- package/dist/service-keys.js.map +1 -0
- package/package.json +40 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
// Imported rather than taken from the global scope: `lib` is ES2023 with no
|
|
4
|
+
// DOM, so the global URL is not typed here.
|
|
5
|
+
import { URL } from 'node:url';
|
|
6
|
+
import { authenticate, rejectTenantOverride } from './auth.js';
|
|
7
|
+
import { badRequest, internal, notFound, Problem } from './errors.js';
|
|
8
|
+
const MAX_BODY_BYTES = 1_000_000;
|
|
9
|
+
const PRINCIPAL_TYPES = ['user', 'group', 'service_account'];
|
|
10
|
+
const PERMISSIONS = ['read', 'write', 'admin'];
|
|
11
|
+
function accountJson(a) {
|
|
12
|
+
return {
|
|
13
|
+
id: a.id,
|
|
14
|
+
name: a.name,
|
|
15
|
+
// Never the key. It exists in the create response and nowhere else.
|
|
16
|
+
key_prefix: a.keyPrefix,
|
|
17
|
+
created_at: a.createdAt,
|
|
18
|
+
last_used_at: a.lastUsedAt,
|
|
19
|
+
revoked_at: a.revokedAt,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function grantJson(g) {
|
|
23
|
+
return {
|
|
24
|
+
id: g.id,
|
|
25
|
+
principal_type: g.principalType,
|
|
26
|
+
principal_id: g.principalId,
|
|
27
|
+
scope_type: g.scopeType,
|
|
28
|
+
scope_id: g.scopeId,
|
|
29
|
+
permission: g.permission,
|
|
30
|
+
effect: g.effect,
|
|
31
|
+
source: g.source,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A grant body, or the reason it is not one.
|
|
36
|
+
*
|
|
37
|
+
* Two of the rejections here are the open/commercial boundary rather than
|
|
38
|
+
* validation. `docs/licensing.md` puts document-level ACLs and deny rules in
|
|
39
|
+
* the commercial half; the core is workspace- and layer-scoped allow-only RBAC.
|
|
40
|
+
* The resolver can already represent both — an enterprise resolver registered
|
|
41
|
+
* through `registerAuthzResolver` produces them — but the core must not issue
|
|
42
|
+
* what it does not implement the propagation for.
|
|
43
|
+
*
|
|
44
|
+
* `400` and not `404`: the caller is an administrator asking for a capability
|
|
45
|
+
* this build does not have, which is not a question about whether an object
|
|
46
|
+
* exists, so invariant I4 has no bearing and saying so plainly is right.
|
|
47
|
+
*/
|
|
48
|
+
function parseGrant(body) {
|
|
49
|
+
const principalType = body.principal_type;
|
|
50
|
+
const principalId = body.principal_id;
|
|
51
|
+
const scopeType = body.scope_type;
|
|
52
|
+
const scopeId = body.scope_id;
|
|
53
|
+
const permission = body.permission;
|
|
54
|
+
const effect = body.effect;
|
|
55
|
+
if (typeof principalId !== 'string' || typeof scopeId !== 'string') {
|
|
56
|
+
return "'principal_id' and 'scope_id' are required.";
|
|
57
|
+
}
|
|
58
|
+
if (typeof principalType !== 'string' || !PRINCIPAL_TYPES.includes(principalType)) {
|
|
59
|
+
return `'principal_type' must be one of ${PRINCIPAL_TYPES.join(', ')}.`;
|
|
60
|
+
}
|
|
61
|
+
if (typeof permission !== 'string' || !PERMISSIONS.includes(permission)) {
|
|
62
|
+
return `'permission' must be one of ${PERMISSIONS.join(', ')}.`;
|
|
63
|
+
}
|
|
64
|
+
if (scopeType === 'document') {
|
|
65
|
+
return 'Document-level grants are not available in this build. Grant on the layer or the workspace instead.';
|
|
66
|
+
}
|
|
67
|
+
if (scopeType !== 'workspace' && scopeType !== 'layer') {
|
|
68
|
+
return "'scope_type' must be 'workspace' or 'layer'.";
|
|
69
|
+
}
|
|
70
|
+
if (effect !== undefined && effect !== 'allow') {
|
|
71
|
+
return 'Deny rules are not available in this build. Grants are allow-only; remove the grant to revoke it.';
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
principalType: principalType,
|
|
75
|
+
principalId,
|
|
76
|
+
scopeType,
|
|
77
|
+
scopeId,
|
|
78
|
+
permission: permission,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function readBody(req) {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let size = 0;
|
|
84
|
+
for await (const chunk of req) {
|
|
85
|
+
size += chunk.length;
|
|
86
|
+
if (size > MAX_BODY_BYTES)
|
|
87
|
+
throw new Error('body too large');
|
|
88
|
+
chunks.push(chunk);
|
|
89
|
+
}
|
|
90
|
+
if (chunks.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
93
|
+
}
|
|
94
|
+
function send(res, status, body, requestId) {
|
|
95
|
+
const json = JSON.stringify(body);
|
|
96
|
+
res.writeHead(status, {
|
|
97
|
+
'content-type': status >= 400 ? 'application/problem+json' : 'application/json',
|
|
98
|
+
'x-request-id': requestId,
|
|
99
|
+
});
|
|
100
|
+
res.end(json);
|
|
101
|
+
}
|
|
102
|
+
export function createApi(options) {
|
|
103
|
+
return createServer((req, res) => {
|
|
104
|
+
void handle(req, res, options).catch(() => {
|
|
105
|
+
// handle() converts everything it can into a Problem. Reaching here means
|
|
106
|
+
// the failure was in the error path itself; say nothing about it.
|
|
107
|
+
if (!res.headersSent)
|
|
108
|
+
send(res, 500, internal(req.url ?? '/', 'unknown').toJSON(), 'unknown');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function handle(req, res, options) {
|
|
113
|
+
const requestId = randomUUID();
|
|
114
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
115
|
+
const instance = url.pathname;
|
|
116
|
+
if (req.method === 'GET' && instance === '/metrics') {
|
|
117
|
+
// Unauthenticated, like every Prometheus endpoint, and therefore carrying
|
|
118
|
+
// nothing that is not already a count. No document ids, no query text, no
|
|
119
|
+
// organization ids — organizations appear by slug, which is in the URL of
|
|
120
|
+
// every request that tenant makes anyway.
|
|
121
|
+
if (options.metrics === undefined) {
|
|
122
|
+
const problem = notFound(instance, requestId);
|
|
123
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const body = await options.metrics.render();
|
|
127
|
+
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' });
|
|
128
|
+
res.end(body);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (req.method === 'GET' && instance === '/v1/health') {
|
|
132
|
+
// Liveness touches no dependency. A health check that calls Postgres turns
|
|
133
|
+
// one slow database into a cascading restart loop.
|
|
134
|
+
send(res, 200, { status: 'ok' }, requestId);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const auth = await authenticate(req.headers.authorization, options.verify, instance, requestId);
|
|
138
|
+
if (auth instanceof Problem) {
|
|
139
|
+
send(res, auth.status, auth.toJSON(), requestId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let body;
|
|
143
|
+
try {
|
|
144
|
+
body = await readBody(req);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
const problem = badRequest(instance, requestId, 'The request body could not be read.');
|
|
148
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// T2. Before routing, before validation: a request that names an organization
|
|
152
|
+
// is not a malformed request, it is an attempt to act as another tenant.
|
|
153
|
+
const override = rejectTenantOverride(body, url.searchParams, req.headers, instance, requestId);
|
|
154
|
+
if (override !== undefined) {
|
|
155
|
+
await options.audit.write({
|
|
156
|
+
orgId: auth.orgId,
|
|
157
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
158
|
+
action: 'tenant_override_attempt',
|
|
159
|
+
result: 'deny',
|
|
160
|
+
detail: { path: instance },
|
|
161
|
+
requestId,
|
|
162
|
+
});
|
|
163
|
+
send(res, override.status, override.toJSON(), requestId);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (req.method === 'POST' && instance === '/v1/search') {
|
|
168
|
+
const query = body?.query;
|
|
169
|
+
const topK = body?.top_k;
|
|
170
|
+
if (typeof query !== 'string' || query.length === 0) {
|
|
171
|
+
const problem = badRequest(instance, requestId, "'query' is required.");
|
|
172
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const results = await options.search.search(auth, query, typeof topK === 'number' ? topK : 10);
|
|
176
|
+
await options.audit.write({
|
|
177
|
+
orgId: auth.orgId,
|
|
178
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
179
|
+
action: 'search',
|
|
180
|
+
result: 'allow',
|
|
181
|
+
detail: { returned: results.length },
|
|
182
|
+
requestId,
|
|
183
|
+
});
|
|
184
|
+
send(res, 200, { items: results }, requestId);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (req.method === 'POST' && instance === '/v1/documents') {
|
|
188
|
+
const body_ = (body ?? {});
|
|
189
|
+
const layer = body_.layer;
|
|
190
|
+
const externalId = body_.external_id;
|
|
191
|
+
const content = body_.content;
|
|
192
|
+
const url_ = body_.url;
|
|
193
|
+
if (typeof layer !== 'string' || typeof externalId !== 'string') {
|
|
194
|
+
const problem = badRequest(instance, requestId, "'layer' and 'external_id' are required.");
|
|
195
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if ((typeof content === 'string') === (typeof url_ === 'string')) {
|
|
199
|
+
// Accepting both would mean choosing silently, and the choice would
|
|
200
|
+
// differ from whatever the caller assumed.
|
|
201
|
+
const problem = badRequest(instance, requestId, "Exactly one of 'content' or 'url' is required.");
|
|
202
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const outcome = await options.ingest.queue(auth, {
|
|
206
|
+
layer,
|
|
207
|
+
externalId,
|
|
208
|
+
...(typeof body_.title === 'string' ? { title: body_.title } : {}),
|
|
209
|
+
...(typeof content === 'string' ? { content } : {}),
|
|
210
|
+
...(typeof url_ === 'string' ? { url: url_ } : {}),
|
|
211
|
+
});
|
|
212
|
+
if (outcome === undefined) {
|
|
213
|
+
// Not 403. A caller without write access must not learn which layers
|
|
214
|
+
// exist by seeing which ones refuse differently from which ones are
|
|
215
|
+
// absent.
|
|
216
|
+
await options.audit.write({
|
|
217
|
+
orgId: auth.orgId,
|
|
218
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
219
|
+
action: 'ingest',
|
|
220
|
+
result: 'deny',
|
|
221
|
+
detail: { layer },
|
|
222
|
+
requestId,
|
|
223
|
+
});
|
|
224
|
+
const problem = notFound(instance, requestId);
|
|
225
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await options.audit.write({
|
|
229
|
+
orgId: auth.orgId,
|
|
230
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
231
|
+
action: 'ingest',
|
|
232
|
+
result: 'allow',
|
|
233
|
+
detail: { document_id: outcome.documentId, unchanged: outcome.unchanged },
|
|
234
|
+
requestId,
|
|
235
|
+
});
|
|
236
|
+
// 200 for an idempotent repeat, 202 for work actually queued. The
|
|
237
|
+
// difference is what lets a client tell "already indexed" from "wait for
|
|
238
|
+
// the job" without polling to find out.
|
|
239
|
+
send(res, outcome.unchanged ? 200 : 202, { document_id: outcome.documentId, job_id: outcome.jobId, status: outcome.unchanged ? 'indexed' : 'queued' }, requestId);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const documentMatch = /^\/v1\/documents\/([^/]+)$/.exec(instance);
|
|
243
|
+
if (req.method === 'DELETE' && documentMatch) {
|
|
244
|
+
const id = decodeURIComponent(documentMatch[1]);
|
|
245
|
+
const removed = await options.ingest.remove(auth, id);
|
|
246
|
+
await options.audit.write({
|
|
247
|
+
orgId: auth.orgId,
|
|
248
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
249
|
+
action: 'delete_document',
|
|
250
|
+
result: removed ? 'allow' : 'deny',
|
|
251
|
+
detail: { document_id: id },
|
|
252
|
+
requestId,
|
|
253
|
+
});
|
|
254
|
+
if (!removed) {
|
|
255
|
+
const problem = notFound(instance, requestId);
|
|
256
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
res.writeHead(204, { 'x-request-id': requestId });
|
|
260
|
+
res.end();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (req.method === 'GET' && documentMatch) {
|
|
264
|
+
const id = decodeURIComponent(documentMatch[1]);
|
|
265
|
+
const document = await options.documents.read(auth, id);
|
|
266
|
+
if (document === undefined) {
|
|
267
|
+
// T8. The same response for "no such document" and "another
|
|
268
|
+
// organization's document". Same status, same body, one code path — two
|
|
269
|
+
// call sites with two messages is how the oracle comes back.
|
|
270
|
+
await options.audit.write({
|
|
271
|
+
orgId: auth.orgId,
|
|
272
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
273
|
+
action: 'get_document',
|
|
274
|
+
result: 'deny',
|
|
275
|
+
detail: { document_id: id },
|
|
276
|
+
requestId,
|
|
277
|
+
});
|
|
278
|
+
const problem = notFound(instance, requestId);
|
|
279
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await options.audit.write({
|
|
283
|
+
orgId: auth.orgId,
|
|
284
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
285
|
+
action: 'get_document',
|
|
286
|
+
result: 'allow',
|
|
287
|
+
detail: { document_id: id },
|
|
288
|
+
requestId,
|
|
289
|
+
});
|
|
290
|
+
send(res, 200, document, requestId);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const jobMatch = /^\/v1\/jobs\/([^/]+)$/.exec(instance);
|
|
294
|
+
if (req.method === 'GET' && jobMatch && options.jobs !== undefined) {
|
|
295
|
+
const id = decodeURIComponent(jobMatch[1]);
|
|
296
|
+
const job = await options.jobs.read(auth, id);
|
|
297
|
+
if (job === undefined) {
|
|
298
|
+
// A job names a document, so it is as much of an oracle as the document
|
|
299
|
+
// is. Absent and another organization's answer identically.
|
|
300
|
+
const problem = notFound(instance, requestId);
|
|
301
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
send(res, 200, {
|
|
305
|
+
job_id: job.jobId,
|
|
306
|
+
document_id: job.documentId,
|
|
307
|
+
status: job.status,
|
|
308
|
+
progress: job.progress,
|
|
309
|
+
...(job.error === undefined ? {} : { error: job.error }),
|
|
310
|
+
}, requestId);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (req.method === 'GET' && instance === '/v1/layers' && options.layers !== undefined) {
|
|
314
|
+
const layers = await options.layers.list(auth);
|
|
315
|
+
// No audit event: this returns what the caller may already read, and one
|
|
316
|
+
// event per listing buries the ones that matter.
|
|
317
|
+
send(res, 200, {
|
|
318
|
+
items: layers.map((l) => ({
|
|
319
|
+
id: l.id,
|
|
320
|
+
slug: l.slug,
|
|
321
|
+
name: l.name,
|
|
322
|
+
workspace_id: l.workspaceId,
|
|
323
|
+
description: l.description,
|
|
324
|
+
document_count: l.documentCount,
|
|
325
|
+
})),
|
|
326
|
+
}, requestId);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (req.method === 'POST' && instance === '/v1/layers' && options.layers !== undefined) {
|
|
330
|
+
const body_ = (body ?? {});
|
|
331
|
+
const workspaceId = body_.workspace_id;
|
|
332
|
+
const slug = body_.slug;
|
|
333
|
+
const name = body_.name;
|
|
334
|
+
if (typeof workspaceId !== 'string' || typeof slug !== 'string' || typeof name !== 'string') {
|
|
335
|
+
const problem = badRequest(instance, requestId, "'workspace_id', 'slug' and 'name' are required.");
|
|
336
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const outcome = await options.layers.create(auth, { workspaceId, slug, name });
|
|
340
|
+
await options.audit.write({
|
|
341
|
+
orgId: auth.orgId,
|
|
342
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
343
|
+
action: 'create_layer',
|
|
344
|
+
result: outcome.kind === 'created' ? 'allow' : 'deny',
|
|
345
|
+
detail: { workspace_id: workspaceId, slug, outcome: outcome.kind },
|
|
346
|
+
requestId,
|
|
347
|
+
});
|
|
348
|
+
if (outcome.kind === 'denied') {
|
|
349
|
+
// 404, not 403. A caller who may not administer a workspace must not be
|
|
350
|
+
// able to tell it apart from one that does not exist.
|
|
351
|
+
const problem = notFound(instance, requestId);
|
|
352
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (outcome.kind === 'conflict') {
|
|
356
|
+
const problem = new Problem({
|
|
357
|
+
type: 'https://nacre.work/errors/conflict',
|
|
358
|
+
title: 'Conflict',
|
|
359
|
+
status: 409,
|
|
360
|
+
detail: `A layer with the slug '${slug}' already exists in this organization.`,
|
|
361
|
+
instance,
|
|
362
|
+
requestId,
|
|
363
|
+
});
|
|
364
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const created = outcome.layer;
|
|
368
|
+
send(res, 201, {
|
|
369
|
+
id: created.id,
|
|
370
|
+
slug: created.slug,
|
|
371
|
+
name: created.name,
|
|
372
|
+
workspace_id: created.workspaceId,
|
|
373
|
+
description: created.description,
|
|
374
|
+
document_count: created.documentCount,
|
|
375
|
+
}, requestId);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (req.method === 'GET' && instance === '/v1/grants' && options.grants !== undefined) {
|
|
379
|
+
const grants = await options.grants.list(auth);
|
|
380
|
+
send(res, 200, { items: grants.map(grantJson) }, requestId);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (req.method === 'POST' && instance === '/v1/grants' && options.grants !== undefined) {
|
|
384
|
+
const body_ = (body ?? {});
|
|
385
|
+
const parsed = parseGrant(body_);
|
|
386
|
+
if (typeof parsed === 'string') {
|
|
387
|
+
const problem = badRequest(instance, requestId, parsed);
|
|
388
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const issued = await options.grants.issue(auth, parsed);
|
|
392
|
+
await options.audit.write({
|
|
393
|
+
orgId: auth.orgId,
|
|
394
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
395
|
+
action: 'issue_grant',
|
|
396
|
+
result: issued === undefined ? 'deny' : 'allow',
|
|
397
|
+
detail: {
|
|
398
|
+
principal: `${parsed.principalType}:${parsed.principalId}`,
|
|
399
|
+
scope: `${parsed.scopeType}:${parsed.scopeId}`,
|
|
400
|
+
permission: parsed.permission,
|
|
401
|
+
},
|
|
402
|
+
requestId,
|
|
403
|
+
});
|
|
404
|
+
if (issued === undefined) {
|
|
405
|
+
const problem = notFound(instance, requestId);
|
|
406
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
send(res, 201, grantJson(issued), requestId);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const grantMatch = /^\/v1\/grants\/([^/]+)$/.exec(instance);
|
|
413
|
+
if (req.method === 'DELETE' && grantMatch && options.grants !== undefined) {
|
|
414
|
+
const id = decodeURIComponent(grantMatch[1]);
|
|
415
|
+
const revoked = await options.grants.revoke(auth, id);
|
|
416
|
+
// Written before the response either way. A revocation nobody can prove
|
|
417
|
+
// happened is not a revocation an auditor will accept, and a *refused*
|
|
418
|
+
// one is what an attempt to revoke someone else's grant looks like.
|
|
419
|
+
await options.audit.write({
|
|
420
|
+
orgId: auth.orgId,
|
|
421
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
422
|
+
action: 'revoke_grant',
|
|
423
|
+
result: revoked ? 'allow' : 'deny',
|
|
424
|
+
detail: { grant_id: id },
|
|
425
|
+
requestId,
|
|
426
|
+
});
|
|
427
|
+
if (!revoked) {
|
|
428
|
+
// 404 for absent, for another organization's, and for one whose scope
|
|
429
|
+
// this caller cannot administer. Distinguishing them would let an
|
|
430
|
+
// administrator of one layer enumerate grants across the organization.
|
|
431
|
+
const problem = notFound(instance, requestId);
|
|
432
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
send(res, 204, null, requestId);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (instance === '/v1/service-accounts' && options.serviceAccounts !== undefined) {
|
|
439
|
+
// org_admin, not "admin on some scope". A service account is a principal
|
|
440
|
+
// in the organization rather than an object inside a workspace, and there
|
|
441
|
+
// is no scope to check it against — someone holding admin on one layer
|
|
442
|
+
// must not be able to mint credentials.
|
|
443
|
+
if (auth.role !== 'org_admin') {
|
|
444
|
+
const problem = notFound(instance, requestId);
|
|
445
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (req.method === 'GET') {
|
|
449
|
+
send(res, 200, { items: (await options.serviceAccounts.list(auth)).map(accountJson) }, requestId);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (req.method === 'POST') {
|
|
453
|
+
const name = (body ?? {}).name;
|
|
454
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
455
|
+
const problem = badRequest(instance, requestId, "'name' is required.");
|
|
456
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const { account, key } = await options.serviceAccounts.create(auth, name.trim());
|
|
460
|
+
await options.audit.write({
|
|
461
|
+
orgId: auth.orgId,
|
|
462
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
463
|
+
action: 'create_service_account',
|
|
464
|
+
result: 'allow',
|
|
465
|
+
// The prefix, never the key. This row is readable by anyone with the
|
|
466
|
+
// audit log, and the key is not recoverable from anywhere else by
|
|
467
|
+
// design — putting it here would undo that.
|
|
468
|
+
detail: { service_account_id: account.id, key_prefix: account.keyPrefix },
|
|
469
|
+
requestId,
|
|
470
|
+
});
|
|
471
|
+
// The only time the key exists outside the caller's process.
|
|
472
|
+
send(res, 201, { ...accountJson(account), key }, requestId);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const accountMatch = /^\/v1\/service-accounts\/([^/]+)$/.exec(instance);
|
|
477
|
+
if (req.method === 'DELETE' && accountMatch && options.serviceAccounts !== undefined) {
|
|
478
|
+
if (auth.role !== 'org_admin') {
|
|
479
|
+
const problem = notFound(instance, requestId);
|
|
480
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const id = decodeURIComponent(accountMatch[1]);
|
|
484
|
+
const revoked = await options.serviceAccounts.revoke(auth, id);
|
|
485
|
+
await options.audit.write({
|
|
486
|
+
orgId: auth.orgId,
|
|
487
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
488
|
+
action: 'revoke_service_account',
|
|
489
|
+
result: revoked ? 'allow' : 'deny',
|
|
490
|
+
detail: { service_account_id: id },
|
|
491
|
+
requestId,
|
|
492
|
+
});
|
|
493
|
+
if (!revoked) {
|
|
494
|
+
const problem = notFound(instance, requestId);
|
|
495
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
send(res, 204, null, requestId);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const problem = notFound(instance, requestId);
|
|
502
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
// The 500 body says "whatever went wrong is in the journal under this
|
|
506
|
+
// request_id", and until this line nothing wrote it there: the error was
|
|
507
|
+
// discarded by a bare `catch`, and the audit row carried an empty detail.
|
|
508
|
+
// A caller reporting a 500 with a request id had nothing to be joined to.
|
|
509
|
+
//
|
|
510
|
+
// The message, not the request. A handler failure can carry a query string
|
|
511
|
+
// or a document body in its cause, and neither belongs in a log — see the
|
|
512
|
+
// list in CLAUDE.md. `String(error)` is the class and the message; the
|
|
513
|
+
// stack goes with it because that is what names the line.
|
|
514
|
+
console.error(JSON.stringify({
|
|
515
|
+
msg: 'request failed',
|
|
516
|
+
request_id: requestId,
|
|
517
|
+
method: req.method,
|
|
518
|
+
instance,
|
|
519
|
+
org_id: auth.orgId,
|
|
520
|
+
error: String(error).slice(0, 500),
|
|
521
|
+
stack: error instanceof Error ? error.stack?.split('\n').slice(0, 6).join('\n') : undefined,
|
|
522
|
+
}));
|
|
523
|
+
await options.audit
|
|
524
|
+
.write({
|
|
525
|
+
orgId: auth.orgId,
|
|
526
|
+
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
527
|
+
action: instance,
|
|
528
|
+
result: 'error',
|
|
529
|
+
// The class alone. An audit row is read by more people than a log line
|
|
530
|
+
// and is retained far longer, so it says what kind of failure it was
|
|
531
|
+
// and points at the log for the rest.
|
|
532
|
+
detail: { error: error instanceof Error ? error.name : 'unknown' },
|
|
533
|
+
requestId,
|
|
534
|
+
})
|
|
535
|
+
.catch((cause) => {
|
|
536
|
+
// Losing the audit row of a failed request is itself worth a line.
|
|
537
|
+
console.error(JSON.stringify({ msg: 'audit write failed', request_id: requestId, error: String(cause) }));
|
|
538
|
+
});
|
|
539
|
+
const problem = internal(instance, requestId);
|
|
540
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { AuthContext } from './auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Service account keys.
|
|
5
|
+
*
|
|
6
|
+
* The one credential in the system meant to outlive a session. `init` issues a
|
|
7
|
+
* token that expires in an hour, which is right for getting through the
|
|
8
|
+
* quickstart and useless for an agent that runs for a week — until this
|
|
9
|
+
* existed, `NACRE_SERVICE_KEY` had nothing to put in it.
|
|
10
|
+
*
|
|
11
|
+
* The shape is not invented here: migration 0001 gave `service_accounts` a
|
|
12
|
+
* `key_hash`, a `key_prefix`, and a `revoked_at`, which is an opaque key stored
|
|
13
|
+
* hashed and looked up by a non-secret prefix. This implements that.
|
|
14
|
+
*
|
|
15
|
+
* A key is shown once, at creation, and never again. Storing anything
|
|
16
|
+
* reversible would make a database backup a set of live credentials, and the
|
|
17
|
+
* whole point of `key_hash` is that a stolen dump is not one.
|
|
18
|
+
*/
|
|
19
|
+
/** Identifies the credential type on sight, in a log or a pasted variable. */
|
|
20
|
+
export declare const KEY_PREFIX = "nacre_sk_";
|
|
21
|
+
/**
|
|
22
|
+
* The lookup prefix: the marker plus the first 8 characters of the secret.
|
|
23
|
+
*
|
|
24
|
+
* Long enough that the indexed lookup returns one row in practice, short enough
|
|
25
|
+
* that it is not a meaningful head start — 8 base64url characters is 48 bits,
|
|
26
|
+
* against a 256-bit secret. It is stored in clear and shown in listings on
|
|
27
|
+
* purpose: an operator revoking a key needs to tell two apart, and the whole
|
|
28
|
+
* key is gone.
|
|
29
|
+
*/
|
|
30
|
+
export declare function prefixOf(key: string): string;
|
|
31
|
+
export declare function hashOf(key: string): string;
|
|
32
|
+
/** 32 bytes from the CSPRNG. Not a UUID — those are not secrets. */
|
|
33
|
+
export declare function generateKey(): string;
|
|
34
|
+
export declare const looksLikeServiceKey: (value: string) => boolean;
|
|
35
|
+
export interface ServiceKeyResolver {
|
|
36
|
+
/** The caller this key belongs to, or undefined for every kind of failure. */
|
|
37
|
+
resolve(key: string): Promise<AuthContext | undefined>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a key against the database.
|
|
41
|
+
*
|
|
42
|
+
* Undefined for unknown, revoked, malformed and wrong alike — the caller turns
|
|
43
|
+
* all of them into the same 401 a bad JWT gets, so which one it was cannot be
|
|
44
|
+
* read off the response.
|
|
45
|
+
*/
|
|
46
|
+
export declare class PostgresServiceKeys implements ServiceKeyResolver {
|
|
47
|
+
private readonly pool;
|
|
48
|
+
private readonly role?;
|
|
49
|
+
constructor(pool: Pool, role?: string | undefined);
|
|
50
|
+
resolve(key: string): Promise<AuthContext | undefined>;
|
|
51
|
+
}
|
|
52
|
+
export interface ServiceAccount {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly keyPrefix: string;
|
|
56
|
+
readonly createdAt: string;
|
|
57
|
+
readonly lastUsedAt: string | null;
|
|
58
|
+
readonly revokedAt: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface ServiceAccounts {
|
|
61
|
+
list(auth: AuthContext): Promise<readonly ServiceAccount[]>;
|
|
62
|
+
/** The key is in the result and nowhere else, ever again. */
|
|
63
|
+
create(auth: AuthContext, name: string): Promise<{
|
|
64
|
+
account: ServiceAccount;
|
|
65
|
+
key: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** False when it does not exist in this organization, or is already revoked. */
|
|
68
|
+
revoke(auth: AuthContext, id: string): Promise<boolean>;
|
|
69
|
+
}
|
|
70
|
+
export declare class PostgresServiceAccounts implements ServiceAccounts {
|
|
71
|
+
private readonly pool;
|
|
72
|
+
private readonly role?;
|
|
73
|
+
constructor(pool: Pool, role?: string | undefined);
|
|
74
|
+
private get scope();
|
|
75
|
+
list(auth: AuthContext): Promise<readonly ServiceAccount[]>;
|
|
76
|
+
create(auth: AuthContext, name: string): Promise<{
|
|
77
|
+
account: ServiceAccount;
|
|
78
|
+
key: string;
|
|
79
|
+
}>;
|
|
80
|
+
revoke(auth: AuthContext, id: string): Promise<boolean>;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=service-keys.d.ts.map
|