@uptimizr/collector-server 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +179 -6
- package/README.md +69 -0
- package/dist/__tests__/support/registryRequests.d.ts +2 -1
- package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
- package/dist/__tests__/support/registryRequests.js +17 -1
- package/dist/__tests__/support/registryRequests.js.map +1 -1
- package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
- package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
- package/dist/__tests__/support/subscriptionFixtures.js +17 -0
- package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
- package/dist/agentReport.d.ts +256 -0
- package/dist/agentReport.d.ts.map +1 -0
- package/dist/agentReport.js +861 -0
- package/dist/agentReport.js.map +1 -0
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -5
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +5 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +24 -4
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +41 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +47 -0
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +99 -2
- package/dist/cli.js.map +1 -1
- package/dist/clickhouseStore.d.ts.map +1 -1
- package/dist/clickhouseStore.js +44 -2
- package/dist/clickhouseStore.js.map +1 -1
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/connectionLimiter.d.ts +23 -0
- package/dist/connectionLimiter.d.ts.map +1 -0
- package/dist/connectionLimiter.js +24 -0
- package/dist/connectionLimiter.js.map +1 -0
- package/dist/duckdbStore.d.ts.map +1 -1
- package/dist/duckdbStore.js +44 -1
- package/dist/duckdbStore.js.map +1 -1
- package/dist/internalDispatch.d.ts +36 -0
- package/dist/internalDispatch.d.ts.map +1 -0
- package/dist/internalDispatch.js +50 -0
- package/dist/internalDispatch.js.map +1 -0
- package/dist/memoryStore.d.ts.map +1 -1
- package/dist/memoryStore.js +314 -1
- package/dist/memoryStore.js.map +1 -1
- package/dist/mssqlStore.d.ts.map +1 -1
- package/dist/mssqlStore.js +44 -2
- package/dist/mssqlStore.js.map +1 -1
- package/dist/postgresStore.d.ts.map +1 -1
- package/dist/postgresStore.js +44 -2
- package/dist/postgresStore.js.map +1 -1
- package/dist/projectMetadata.d.ts +82 -0
- package/dist/projectMetadata.d.ts.map +1 -0
- package/dist/projectMetadata.js +50 -0
- package/dist/projectMetadata.js.map +1 -0
- package/dist/routes/context.d.ts +135 -0
- package/dist/routes/context.d.ts.map +1 -0
- package/dist/routes/context.js +306 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/insights.d.ts +36 -0
- package/dist/routes/insights.d.ts.map +1 -0
- package/dist/routes/insights.js +810 -0
- package/dist/routes/insights.js.map +1 -0
- package/dist/routes/live.d.ts +8 -0
- package/dist/routes/live.d.ts.map +1 -1
- package/dist/routes/live.js +8 -7
- package/dist/routes/live.js.map +1 -1
- package/dist/routes/mcp.d.ts +36 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +262 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/meta.d.ts +16 -2
- package/dist/routes/meta.d.ts.map +1 -1
- package/dist/routes/meta.js +699 -10
- package/dist/routes/meta.js.map +1 -1
- package/dist/routes/metadata.d.ts +31 -0
- package/dist/routes/metadata.d.ts.map +1 -0
- package/dist/routes/metadata.js +163 -0
- package/dist/routes/metadata.js.map +1 -0
- package/dist/routes/narrative.d.ts +36 -0
- package/dist/routes/narrative.d.ts.map +1 -0
- package/dist/routes/narrative.js +95 -0
- package/dist/routes/narrative.js.map +1 -0
- package/dist/routes/panels.d.ts +44 -0
- package/dist/routes/panels.d.ts.map +1 -0
- package/dist/routes/panels.js +108 -0
- package/dist/routes/panels.js.map +1 -0
- package/dist/routes/query-dsl.d.ts +50 -0
- package/dist/routes/query-dsl.d.ts.map +1 -0
- package/dist/routes/query-dsl.js +359 -0
- package/dist/routes/query-dsl.js.map +1 -0
- package/dist/routes/query.d.ts +36 -0
- package/dist/routes/query.d.ts.map +1 -1
- package/dist/routes/query.js +101 -13
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/subscriptions.d.ts +52 -0
- package/dist/routes/subscriptions.d.ts.map +1 -0
- package/dist/routes/subscriptions.js +290 -0
- package/dist/routes/subscriptions.js.map +1 -0
- package/dist/store.d.ts +113 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/subscriptions/cli.d.ts +36 -0
- package/dist/subscriptions/cli.d.ts.map +1 -0
- package/dist/subscriptions/cli.js +143 -0
- package/dist/subscriptions/cli.js.map +1 -0
- package/dist/subscriptions/delivery.d.ts +101 -0
- package/dist/subscriptions/delivery.d.ts.map +1 -0
- package/dist/subscriptions/delivery.js +165 -0
- package/dist/subscriptions/delivery.js.map +1 -0
- package/dist/subscriptions/evaluate.d.ts +119 -0
- package/dist/subscriptions/evaluate.d.ts.map +1 -0
- package/dist/subscriptions/evaluate.js +326 -0
- package/dist/subscriptions/evaluate.js.map +1 -0
- package/dist/subscriptions/scheduler.d.ts +97 -0
- package/dist/subscriptions/scheduler.d.ts.map +1 -0
- package/dist/subscriptions/scheduler.js +332 -0
- package/dist/subscriptions/scheduler.js.map +1 -0
- package/dist/subscriptions/stream.d.ts +40 -0
- package/dist/subscriptions/stream.d.ts.map +1 -0
- package/dist/subscriptions/stream.js +38 -0
- package/dist/subscriptions/stream.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/dist/webhookSignature.d.ts +64 -0
- package/dist/webhookSignature.d.ts.map +1 -0
- package/dist/webhookSignature.js +91 -0
- package/dist/webhookSignature.js.map +1 -0
- package/llms.txt +38 -1
- package/package.json +17 -14
package/dist/routes/meta.js
CHANGED
|
@@ -22,14 +22,42 @@
|
|
|
22
22
|
* in `app.ts`).
|
|
23
23
|
*
|
|
24
24
|
* Routes that are not registry metrics are described only when they are trivial
|
|
25
|
-
* and honest to describe (`/health`, this route,
|
|
26
|
-
* listing
|
|
25
|
+
* and honest to describe (`/health`, this route, the scene-representation
|
|
26
|
+
* listing, and the `metadata` group of #310 — annotations, glossary, saved
|
|
27
|
+
* analyses — whose bodies are converted from the Zod contracts that validate
|
|
28
|
+
* them). Ingestion (`POST /api/v1/collect`), the scene-proxy write
|
|
27
29
|
* (`PUT …/representation`), the retention-gated raw event stream and the live
|
|
28
30
|
* SSE surface are deliberately omitted rather than half-described.
|
|
31
|
+
* listing, and the conditional-subscription resource). Ingestion
|
|
32
|
+
* (`POST /api/v1/collect`), the scene-proxy write (`PUT …/representation`), the
|
|
33
|
+
* retention-gated raw event stream and every SSE surface — the live endpoints
|
|
34
|
+
* and `GET /api/v1/subscriptions/stream` alike — are deliberately omitted rather
|
|
35
|
+
* than half-described: a hijacked `text/event-stream` response is not an
|
|
36
|
+
* operation with a JSON body, and pretending otherwise misleads a generated
|
|
37
|
+
* client.
|
|
38
|
+
*
|
|
39
|
+
* Conditional subscriptions (#311) are the one **write** surface described here.
|
|
40
|
+
* They earn it: they are a CRUD resource rather than a query, and the only place
|
|
41
|
+
* a caller needs a written contract for a request *body* — a closed Zod union
|
|
42
|
+
* that cannot be inferred from a querystring.
|
|
29
43
|
*/
|
|
30
44
|
import { z } from "zod";
|
|
31
|
-
import { SCHEMA_VERSION } from "@uptimizr/schema";
|
|
32
|
-
import {
|
|
45
|
+
import { LIMITS, SCHEMA_VERSION, annotationSchema, panelSpecV1Schema, queryV1Schema, savedAnalysisSchema, } from "@uptimizr/schema";
|
|
46
|
+
import { projectContextSchema } from "./context.js";
|
|
47
|
+
import { FILTER_TARGETS, metricCapability, allMetrics, isResourceMetric, } from "@uptimizr/metrics";
|
|
48
|
+
/**
|
|
49
|
+
* The project context document's JSON Schema, derived from the very Zod schema
|
|
50
|
+
* that serialises the response (`routes/context.ts`) rather than restated here —
|
|
51
|
+
* the same rule the registry rows follow, for the same reason.
|
|
52
|
+
*/
|
|
53
|
+
function contextSchema() {
|
|
54
|
+
const schema = z.toJSONSchema(projectContextSchema, {
|
|
55
|
+
io: "output",
|
|
56
|
+
unrepresentable: "any",
|
|
57
|
+
});
|
|
58
|
+
delete schema.$schema;
|
|
59
|
+
return { title: "Project context", ...schema };
|
|
60
|
+
}
|
|
33
61
|
/**
|
|
34
62
|
* `info.version` of the served document: the major version of the HTTP API
|
|
35
63
|
* (`/api/v1`). It is deliberately *not* the npm package version — the document
|
|
@@ -207,12 +235,21 @@ function vendorExtensions(metric) {
|
|
|
207
235
|
...(label ? { "x-uptimizr-label": label } : {}),
|
|
208
236
|
"x-uptimizr-limits": metric.limits,
|
|
209
237
|
"x-uptimizr-interpretation": metric.interpretation,
|
|
238
|
+
"x-uptimizr-capability": metricCapability(metric),
|
|
210
239
|
"x-uptimizr-caveats": metric.caveats,
|
|
211
240
|
"x-uptimizr-source-channels": metric.sourceChannels,
|
|
212
241
|
"x-uptimizr-related": metric.related,
|
|
213
242
|
...(metric.comparable ? { "x-uptimizr-comparable": metric.comparable } : {}),
|
|
214
243
|
};
|
|
215
244
|
}
|
|
245
|
+
/** The `{id}` path parameter shared by every per-subscription operation. */
|
|
246
|
+
const SUBSCRIPTION_ID_PARAM = {
|
|
247
|
+
name: "id",
|
|
248
|
+
in: "path",
|
|
249
|
+
required: true,
|
|
250
|
+
schema: { type: "string", maxLength: 64 },
|
|
251
|
+
description: "Collector-assigned subscription id (`sub_…`).",
|
|
252
|
+
};
|
|
216
253
|
/** The shared error responses every authenticated read can answer with. */
|
|
217
254
|
const AUTHENTICATED_ERRORS = {
|
|
218
255
|
"400": { $ref: "#/components/responses/BadRequest" },
|
|
@@ -250,6 +287,68 @@ function operationFor(metric, route) {
|
|
|
250
287
|
...vendorExtensions(metric),
|
|
251
288
|
};
|
|
252
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The **query DSL** operations (ADR 0051 §3). Unlike the paths above, these are
|
|
292
|
+
* not generated from a registry entry — one route serves every metric — so the
|
|
293
|
+
* operation is described here and its request body is the `queryV1` Zod schema
|
|
294
|
+
* converted to JSON Schema, which is the same schema that validates the request.
|
|
295
|
+
*
|
|
296
|
+
* The response is deliberately loose: the row shape is the metric's, and the
|
|
297
|
+
* document already describes every metric's row under `components.schemas`.
|
|
298
|
+
*/
|
|
299
|
+
function queryDslPath() {
|
|
300
|
+
const body = toJsonSchema(queryV1Schema, "input");
|
|
301
|
+
body.title = "Analytics query (v1)";
|
|
302
|
+
const description = "Run **any** registry metric in one validated request: pick the `metric`, bound it with a " +
|
|
303
|
+
"`range`, narrow it with the filters that metric declares, cap it with `limit`, and choose " +
|
|
304
|
+
"the result envelope with `format` (`table` by default).\n\n" +
|
|
305
|
+
"The grammar is **closed**: there is no raw SQL and no free-form expression, and the metric, " +
|
|
306
|
+
"dimension and filter vocabularies are exactly this document's. A metric that does not accept " +
|
|
307
|
+
"a filter, a dimension it is not keyed by, or a `limit` above its cap is a `400` naming what " +
|
|
308
|
+
"it does accept.\n\n" +
|
|
309
|
+
"**v1 is the delegated tier**: a query runs the metric's existing aggregation, so it is " +
|
|
310
|
+
"reachable at exactly the power of its canned endpoint. `compare`, `segment`, `order`, " +
|
|
311
|
+
"`explain`, `filters.event` and `filters.device` are part of the published grammar and are " +
|
|
312
|
+
"answered with `400 … not supported yet` until the generic group-by tier lands.";
|
|
313
|
+
const responses = {
|
|
314
|
+
"200": {
|
|
315
|
+
description: "The metric's rows (`format=full`), the rows plus a `meta` envelope (`table`), or a " +
|
|
316
|
+
"bounded digest (`summary`).",
|
|
317
|
+
content: { "application/json": { schema: { type: "object" } } },
|
|
318
|
+
},
|
|
319
|
+
...AUTHENTICATED_ERRORS,
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
"/api/v1/query": {
|
|
323
|
+
post: {
|
|
324
|
+
operationId: "query",
|
|
325
|
+
summary: "Run an analytics query",
|
|
326
|
+
description,
|
|
327
|
+
tags: ["meta"],
|
|
328
|
+
requestBody: { required: true, content: { "application/json": { schema: body } } },
|
|
329
|
+
responses,
|
|
330
|
+
},
|
|
331
|
+
get: {
|
|
332
|
+
operationId: "query_get",
|
|
333
|
+
summary: "Run an analytics query (GET form)",
|
|
334
|
+
description: "The same query as `POST /api/v1/query`, URL-encoded into a single `q` parameter, for " +
|
|
335
|
+
"GET-only clients.\n\n" +
|
|
336
|
+
description,
|
|
337
|
+
tags: ["meta"],
|
|
338
|
+
parameters: [
|
|
339
|
+
{
|
|
340
|
+
name: "q",
|
|
341
|
+
in: "query",
|
|
342
|
+
required: true,
|
|
343
|
+
description: "The query document, JSON-encoded and URL-encoded (at most 8 KiB).",
|
|
344
|
+
schema: { type: "string", minLength: 1, maxLength: 8192 },
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
responses,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
253
352
|
/** The handful of non-metric routes that are trivial and honest to describe. */
|
|
254
353
|
function staticPaths() {
|
|
255
354
|
return {
|
|
@@ -291,6 +390,26 @@ function staticPaths() {
|
|
|
291
390
|
},
|
|
292
391
|
},
|
|
293
392
|
},
|
|
393
|
+
"/api/v1/context": {
|
|
394
|
+
get: {
|
|
395
|
+
operationId: "project_context",
|
|
396
|
+
summary: "Project context document",
|
|
397
|
+
description: "Everything an agent needs to know about *this* project before it asks anything: the store engine and versions, data freshness and retention flags, which capture channels produce data, the scenes with their labels and named regions, the discovered custom-event/mesh/input vocabulary, the glossary and recent annotations, and which registry metrics are disabled because their capture channels are off (ADR 0051 §5). Bounded and cached briefly per project. Not a registry metric — it describes the project, not a row grain.",
|
|
398
|
+
tags: ["meta"],
|
|
399
|
+
parameters: [],
|
|
400
|
+
responses: {
|
|
401
|
+
"200": {
|
|
402
|
+
description: "The project context document.",
|
|
403
|
+
content: {
|
|
404
|
+
"application/json": {
|
|
405
|
+
schema: { $ref: "#/components/schemas/ProjectContext" },
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
...AUTHENTICATED_ERRORS,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
294
413
|
"/api/v1/scene-representations": {
|
|
295
414
|
get: {
|
|
296
415
|
operationId: "list_scene_representations",
|
|
@@ -314,6 +433,492 @@ function staticPaths() {
|
|
|
314
433
|
},
|
|
315
434
|
},
|
|
316
435
|
},
|
|
436
|
+
// --- Conditional subscriptions (#311, ADR 0051 §6 / sketch §F.1–F.3) ----
|
|
437
|
+
// Described here rather than derived, because a subscription is project
|
|
438
|
+
// *configuration* and not a metric: it has no rows, no grain and no window,
|
|
439
|
+
// so there is nothing in the registry to generate an operation from.
|
|
440
|
+
//
|
|
441
|
+
// The SSE stream stays omitted, for the reason the module note gives: a
|
|
442
|
+
// hijacked `text/event-stream` response is not honestly describable as a
|
|
443
|
+
// JSON operation, and a half-description is worse than the docs-site prose.
|
|
444
|
+
"/api/v1/subscriptions": {
|
|
445
|
+
get: {
|
|
446
|
+
operationId: "list_subscriptions",
|
|
447
|
+
summary: "Conditional subscriptions",
|
|
448
|
+
description: "The project's standing conditional subscriptions (ADR 0051 §6): what the collector watches for, how often it checks, where a firing is delivered, and how each one last went. Any webhook secret is masked. Needs the `query` capability.",
|
|
449
|
+
tags: ["subscriptions"],
|
|
450
|
+
parameters: [],
|
|
451
|
+
responses: {
|
|
452
|
+
"200": {
|
|
453
|
+
description: "One entry per subscription, oldest first.",
|
|
454
|
+
content: {
|
|
455
|
+
"application/json": {
|
|
456
|
+
schema: { type: "array", items: { $ref: "#/components/schemas/Subscription" } },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
...AUTHENTICATED_ERRORS,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
post: {
|
|
464
|
+
operationId: "create_subscription",
|
|
465
|
+
summary: "Create a conditional subscription",
|
|
466
|
+
description: "Store a new subscription. Needs the `annotate` capability, because creating one is how a caller asks the collector to make an outbound request on its behalf. A webhook `secret` is write-only: it is accepted here and never returned again. A webhook URL must be http(s) and its host must appear in `COLLECTOR_WEBHOOK_ALLOWED_HOSTS`, or the request is refused.",
|
|
467
|
+
tags: ["subscriptions"],
|
|
468
|
+
requestBody: {
|
|
469
|
+
required: true,
|
|
470
|
+
content: {
|
|
471
|
+
"application/json": { schema: { $ref: "#/components/schemas/Subscription" } },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
responses: {
|
|
475
|
+
"201": {
|
|
476
|
+
description: "The stored subscription, with any secret masked.",
|
|
477
|
+
content: {
|
|
478
|
+
"application/json": { schema: { $ref: "#/components/schemas/Subscription" } },
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
"409": {
|
|
482
|
+
description: "The project already holds the maximum number of subscriptions.",
|
|
483
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
484
|
+
},
|
|
485
|
+
...AUTHENTICATED_ERRORS,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
"/api/v1/subscriptions/{id}": {
|
|
490
|
+
get: {
|
|
491
|
+
operationId: "get_subscription",
|
|
492
|
+
summary: "One conditional subscription",
|
|
493
|
+
tags: ["subscriptions"],
|
|
494
|
+
parameters: [SUBSCRIPTION_ID_PARAM],
|
|
495
|
+
responses: {
|
|
496
|
+
"200": {
|
|
497
|
+
description: "The subscription.",
|
|
498
|
+
content: {
|
|
499
|
+
"application/json": { schema: { $ref: "#/components/schemas/Subscription" } },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
503
|
+
...AUTHENTICATED_ERRORS,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
patch: {
|
|
507
|
+
operationId: "set_subscription_enabled",
|
|
508
|
+
summary: "Enable or disable a subscription",
|
|
509
|
+
description: "The only patchable field is `enabled`. Everything else changes what the subscription *means*, so it is replaced rather than edited. Needs `annotate`.",
|
|
510
|
+
tags: ["subscriptions"],
|
|
511
|
+
parameters: [SUBSCRIPTION_ID_PARAM],
|
|
512
|
+
requestBody: {
|
|
513
|
+
required: true,
|
|
514
|
+
content: {
|
|
515
|
+
"application/json": {
|
|
516
|
+
schema: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: { enabled: { type: "boolean" } },
|
|
519
|
+
required: ["enabled"],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
responses: {
|
|
525
|
+
"200": {
|
|
526
|
+
description: "The updated subscription.",
|
|
527
|
+
content: {
|
|
528
|
+
"application/json": { schema: { $ref: "#/components/schemas/Subscription" } },
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
532
|
+
...AUTHENTICATED_ERRORS,
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
delete: {
|
|
536
|
+
operationId: "delete_subscription",
|
|
537
|
+
summary: "Delete a subscription and its firing log",
|
|
538
|
+
tags: ["subscriptions"],
|
|
539
|
+
parameters: [SUBSCRIPTION_ID_PARAM],
|
|
540
|
+
responses: {
|
|
541
|
+
"204": { description: "Deleted." },
|
|
542
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
543
|
+
...AUTHENTICATED_ERRORS,
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
"/api/v1/subscriptions/{id}/events": {
|
|
548
|
+
get: {
|
|
549
|
+
operationId: "list_subscription_events",
|
|
550
|
+
summary: "Recent firings of one subscription",
|
|
551
|
+
description: "The bounded firing log — the last 100 firings, newest first. Each row is { id, subscriptionId, projectId, at, payload }, where `payload` is the firing record plus the `format=summary` block that was delivered with it.",
|
|
552
|
+
tags: ["subscriptions"],
|
|
553
|
+
parameters: [
|
|
554
|
+
SUBSCRIPTION_ID_PARAM,
|
|
555
|
+
{
|
|
556
|
+
name: "limit",
|
|
557
|
+
in: "query",
|
|
558
|
+
required: false,
|
|
559
|
+
schema: { type: "integer", minimum: 1, maximum: 100 },
|
|
560
|
+
description: "Newest-first cap; clamped to the retained window.",
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
responses: {
|
|
564
|
+
"200": {
|
|
565
|
+
description: "One entry per recorded firing.",
|
|
566
|
+
content: {
|
|
567
|
+
"application/json": { schema: { type: "array", items: { type: "object" } } },
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
571
|
+
...AUTHENTICATED_ERRORS,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
"/api/v1/subscriptions/{id}/test": {
|
|
576
|
+
post: {
|
|
577
|
+
operationId: "test_subscription",
|
|
578
|
+
summary: "Evaluate a subscription once, now",
|
|
579
|
+
description: "Runs the predicate immediately and answers with the evaluation, including why it did **not** fire. A dry run by default; `deliver=true` records and delivers a real firing (honouring the cooldown), which is how a webhook receiver is proved to work without waiting for the condition to occur. Needs `annotate`.",
|
|
580
|
+
tags: ["subscriptions"],
|
|
581
|
+
parameters: [
|
|
582
|
+
SUBSCRIPTION_ID_PARAM,
|
|
583
|
+
{
|
|
584
|
+
name: "deliver",
|
|
585
|
+
in: "query",
|
|
586
|
+
required: false,
|
|
587
|
+
schema: { type: "boolean", default: false },
|
|
588
|
+
description: "Record and deliver the firing if the predicate is satisfied.",
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
responses: {
|
|
592
|
+
"200": {
|
|
593
|
+
description: "The evaluation.",
|
|
594
|
+
content: { "application/json": { schema: { type: "object" } } },
|
|
595
|
+
},
|
|
596
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
597
|
+
"409": {
|
|
598
|
+
description: "An evaluation of this subscription is already in flight.",
|
|
599
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
600
|
+
},
|
|
601
|
+
...AUTHENTICATED_ERRORS,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The **metadata** paths (#310, ADR 0051 §5): annotations, glossary, saved
|
|
609
|
+
* analyses. They are not registry metrics — they store what people and agents
|
|
610
|
+
* write rather than aggregate what visitors did — so they are described by hand
|
|
611
|
+
* here, next to the other non-metric routes. The two request bodies are
|
|
612
|
+
* converted from the very Zod contracts that validate them, so the document
|
|
613
|
+
* cannot drift from the validator.
|
|
614
|
+
*/
|
|
615
|
+
function metadataPaths() {
|
|
616
|
+
const jsonBody = (schema) => ({
|
|
617
|
+
required: true,
|
|
618
|
+
content: { "application/json": { schema } },
|
|
619
|
+
});
|
|
620
|
+
const rows = (name) => ({
|
|
621
|
+
"application/json": {
|
|
622
|
+
schema: { type: "array", items: { $ref: `#/components/schemas/${name}` } },
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const one = (name) => ({
|
|
626
|
+
"application/json": { schema: { $ref: `#/components/schemas/${name}` } },
|
|
627
|
+
});
|
|
628
|
+
const writeErrors = {
|
|
629
|
+
...AUTHENTICATED_ERRORS,
|
|
630
|
+
"409": { $ref: "#/components/responses/Conflict" },
|
|
631
|
+
};
|
|
632
|
+
const notFound = { "404": { $ref: "#/components/responses/NotFound" } };
|
|
633
|
+
return {
|
|
634
|
+
"/api/v1/annotations": {
|
|
635
|
+
get: {
|
|
636
|
+
operationId: "list_annotations",
|
|
637
|
+
summary: "Project annotations",
|
|
638
|
+
description: "Notes left on the project, newest first. `since`/`until` select the annotations whose period **overlaps** the window; a standing note (no period) always matches. Requires the `query` capability.",
|
|
639
|
+
tags: ["metadata"],
|
|
640
|
+
responses: {
|
|
641
|
+
"200": { description: "Matching annotations.", content: rows("Annotation") },
|
|
642
|
+
...AUTHENTICATED_ERRORS,
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
post: {
|
|
646
|
+
operationId: "create_annotation",
|
|
647
|
+
summary: "Annotate something",
|
|
648
|
+
description: "Leave a note on the project, a scene, a mesh, a region, a metric or a period of time. Requires the **`annotate`** capability; the stored row records whether a person or an agent wrote it, and the write is recorded in the agent audit log.",
|
|
649
|
+
tags: ["metadata"],
|
|
650
|
+
requestBody: jsonBody(toJsonSchema(annotationSchema, "input")),
|
|
651
|
+
responses: {
|
|
652
|
+
"201": { description: "The stored annotation.", content: one("Annotation") },
|
|
653
|
+
...writeErrors,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
"/api/v1/annotations/{id}": {
|
|
658
|
+
delete: {
|
|
659
|
+
operationId: "delete_annotation",
|
|
660
|
+
summary: "Delete an annotation",
|
|
661
|
+
description: "Remove one annotation of this project. Requires the `annotate` capability. An id that does not exist — or belongs to another project — answers 404.",
|
|
662
|
+
tags: ["metadata"],
|
|
663
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
664
|
+
responses: { "204": { description: "Deleted." }, ...AUTHENTICATED_ERRORS, ...notFound },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
"/api/v1/glossary": {
|
|
668
|
+
get: {
|
|
669
|
+
operationId: "list_glossary",
|
|
670
|
+
summary: "Project glossary",
|
|
671
|
+
description: "What this project's names mean, ordered by term — the vocabulary to read before interpreting mesh names, scene ids and custom events. Requires the `query` capability.",
|
|
672
|
+
tags: ["metadata"],
|
|
673
|
+
responses: {
|
|
674
|
+
"200": { description: "The whole glossary.", content: rows("GlossaryEntry") },
|
|
675
|
+
...AUTHENTICATED_ERRORS,
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
"/api/v1/glossary/{term}": {
|
|
680
|
+
put: {
|
|
681
|
+
operationId: "define_term",
|
|
682
|
+
summary: "Define a term",
|
|
683
|
+
description: "Idempotent upsert: the term is the identity, so defining it again replaces the meaning. Requires the **`annotate`** capability.",
|
|
684
|
+
tags: ["metadata"],
|
|
685
|
+
parameters: [{ name: "term", in: "path", required: true, schema: { type: "string" } }],
|
|
686
|
+
requestBody: jsonBody({
|
|
687
|
+
type: "object",
|
|
688
|
+
properties: {
|
|
689
|
+
meaning: {
|
|
690
|
+
type: "string",
|
|
691
|
+
maxLength: LIMITS.maxGlossaryMeaningLength,
|
|
692
|
+
description: "What the term means in this project.",
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
required: ["meaning"],
|
|
696
|
+
}),
|
|
697
|
+
responses: {
|
|
698
|
+
"200": { description: "The stored entry.", content: one("GlossaryEntry") },
|
|
699
|
+
...writeErrors,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
delete: {
|
|
703
|
+
operationId: "delete_term",
|
|
704
|
+
summary: "Undefine a term",
|
|
705
|
+
description: "Remove one glossary entry. Requires the `annotate` capability.",
|
|
706
|
+
tags: ["metadata"],
|
|
707
|
+
parameters: [{ name: "term", in: "path", required: true, schema: { type: "string" } }],
|
|
708
|
+
responses: { "204": { description: "Deleted." }, ...AUTHENTICATED_ERRORS, ...notFound },
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
"/api/v1/analyses": {
|
|
712
|
+
get: {
|
|
713
|
+
operationId: "list_analyses",
|
|
714
|
+
summary: "Saved analyses",
|
|
715
|
+
description: "Questions worth re-asking and what was concluded from them, newest first. Requires the `query` capability.",
|
|
716
|
+
tags: ["metadata"],
|
|
717
|
+
responses: {
|
|
718
|
+
"200": { description: "Saved analyses.", content: rows("SavedAnalysis") },
|
|
719
|
+
...AUTHENTICATED_ERRORS,
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
post: {
|
|
723
|
+
operationId: "save_analysis",
|
|
724
|
+
summary: "Save an analysis",
|
|
725
|
+
description: "Store a titled question plus the finding it produced. `query` is an opaque JSON document the collector stores but does not interpret. Requires the **`annotate`** capability.",
|
|
726
|
+
tags: ["metadata"],
|
|
727
|
+
requestBody: jsonBody(toJsonSchema(savedAnalysisSchema, "input")),
|
|
728
|
+
responses: {
|
|
729
|
+
"201": { description: "The stored analysis.", content: one("SavedAnalysis") },
|
|
730
|
+
...writeErrors,
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
"/api/v1/analyses/{id}": {
|
|
735
|
+
delete: {
|
|
736
|
+
operationId: "delete_analysis",
|
|
737
|
+
summary: "Delete a saved analysis",
|
|
738
|
+
description: "Remove one saved analysis. Requires the `annotate` capability.",
|
|
739
|
+
tags: ["metadata"],
|
|
740
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
741
|
+
responses: { "204": { description: "Deleted." }, ...AUTHENTICATED_ERRORS, ...notFound },
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The **panels** paths (#315, ADR 0051 §7): the declarative panel specs an
|
|
748
|
+
* agent pins to the dashboard. Like the metadata group these are not registry
|
|
749
|
+
* metrics, so they are described by hand here; the request body is converted
|
|
750
|
+
* from the very Zod contract that validates it, so the document cannot drift
|
|
751
|
+
* from the validator.
|
|
752
|
+
*/
|
|
753
|
+
function panelPaths() {
|
|
754
|
+
const rows = {
|
|
755
|
+
"application/json": {
|
|
756
|
+
schema: { type: "array", items: { $ref: "#/components/schemas/PanelSpec" } },
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
const one = { "application/json": { schema: { $ref: "#/components/schemas/PanelSpec" } } };
|
|
760
|
+
const specBody = {
|
|
761
|
+
required: true,
|
|
762
|
+
content: { "application/json": { schema: toJsonSchema(panelSpecV1Schema, "input") } },
|
|
763
|
+
};
|
|
764
|
+
const idParam = [{ name: "id", in: "path", required: true, schema: { type: "string" } }];
|
|
765
|
+
const refusal = "A spec whose query the registry cannot answer — an unknown metric, a filter the metric " +
|
|
766
|
+
"does not accept — or whose `chart` does not suit the metric's grain, or whose `encoding` " +
|
|
767
|
+
"names a column the result does not carry, is refused with the validator's issue codes, so " +
|
|
768
|
+
"the spec can be fixed from the response rather than guessed at.";
|
|
769
|
+
return {
|
|
770
|
+
"/api/v1/panels": {
|
|
771
|
+
get: {
|
|
772
|
+
operationId: "list_panels",
|
|
773
|
+
summary: "Pinned panels",
|
|
774
|
+
description: "The project's declarative panel specs, **oldest first** — these are positions in a " +
|
|
775
|
+
"dashboard grid rather than a feed, so a newly pinned panel is appended instead of " +
|
|
776
|
+
"displacing the ones already there. Requires the `query` capability.",
|
|
777
|
+
tags: ["panels"],
|
|
778
|
+
responses: {
|
|
779
|
+
"200": { description: "The pinned panels.", content: rows },
|
|
780
|
+
...AUTHENTICATED_ERRORS,
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
post: {
|
|
784
|
+
operationId: "pin_panel",
|
|
785
|
+
summary: "Pin a panel",
|
|
786
|
+
description: "Store a query, a chart and a one-line reading as a panel the dashboard renders on " +
|
|
787
|
+
"every load. The spec is **data, never code**: no module is loaded and nothing is " +
|
|
788
|
+
"evaluated, so pinning a panel does not widen the dashboard's trust boundary " +
|
|
789
|
+
"(ADR 0041). Requires the **`annotate`** capability; the stored row records whether a " +
|
|
790
|
+
"person or an agent pinned it, and the write is recorded in the agent audit log.\n\n" +
|
|
791
|
+
refusal,
|
|
792
|
+
tags: ["panels"],
|
|
793
|
+
requestBody: specBody,
|
|
794
|
+
responses: {
|
|
795
|
+
"201": { description: "The stored panel.", content: one },
|
|
796
|
+
...AUTHENTICATED_ERRORS,
|
|
797
|
+
"409": { $ref: "#/components/responses/Conflict" },
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
"/api/v1/panels/{id}": {
|
|
802
|
+
put: {
|
|
803
|
+
operationId: "update_panel",
|
|
804
|
+
summary: "Replace a pinned panel's spec",
|
|
805
|
+
description: "A full replacement rather than a patch — the spec is one closed document, and half " +
|
|
806
|
+
"of one is not a panel. The row keeps its id, its place in the grid and its original " +
|
|
807
|
+
"authorship: an edit is not a new pin. Requires the `annotate` capability.\n\n" +
|
|
808
|
+
refusal,
|
|
809
|
+
tags: ["panels"],
|
|
810
|
+
parameters: idParam,
|
|
811
|
+
requestBody: specBody,
|
|
812
|
+
responses: {
|
|
813
|
+
"200": { description: "The stored panel.", content: one },
|
|
814
|
+
...AUTHENTICATED_ERRORS,
|
|
815
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
delete: {
|
|
819
|
+
operationId: "unpin_panel",
|
|
820
|
+
summary: "Unpin a panel",
|
|
821
|
+
description: "Remove one panel from the project's dashboard. Requires the `annotate` capability. " +
|
|
822
|
+
"An id that does not exist — or belongs to another project — answers 404.",
|
|
823
|
+
tags: ["panels"],
|
|
824
|
+
parameters: idParam,
|
|
825
|
+
responses: {
|
|
826
|
+
"204": { description: "Unpinned." },
|
|
827
|
+
...AUTHENTICATED_ERRORS,
|
|
828
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/** The stored-row schemas of the three metadata tables. */
|
|
835
|
+
function metadataSchemas() {
|
|
836
|
+
const author = {
|
|
837
|
+
authorKind: {
|
|
838
|
+
type: "string",
|
|
839
|
+
enum: ["user", "agent"],
|
|
840
|
+
description: "Whether a person (a first-party UI session) or an agent wrote the row. Derived by the collector from the calling client, never from the payload.",
|
|
841
|
+
},
|
|
842
|
+
authorKeyId: {
|
|
843
|
+
type: ["string", "null"],
|
|
844
|
+
description: "Id of the API key that carried the write — never the key or its hash.",
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
return {
|
|
848
|
+
Annotation: {
|
|
849
|
+
type: "object",
|
|
850
|
+
title: "Annotation",
|
|
851
|
+
properties: {
|
|
852
|
+
id: { type: "string" },
|
|
853
|
+
projectId: { type: "string" },
|
|
854
|
+
targetKind: {
|
|
855
|
+
type: "string",
|
|
856
|
+
enum: ["project", "scene", "mesh", "region", "metric", "window"],
|
|
857
|
+
description: "What the note is about.",
|
|
858
|
+
},
|
|
859
|
+
targetId: {
|
|
860
|
+
type: ["string", "null"],
|
|
861
|
+
description: "The scene id, mesh name, region id or metric id the note points at.",
|
|
862
|
+
},
|
|
863
|
+
since: {
|
|
864
|
+
type: ["string", "null"],
|
|
865
|
+
description: "Start of the annotated period (ISO 8601).",
|
|
866
|
+
},
|
|
867
|
+
until: { type: ["string", "null"], description: "End of the annotated period (ISO 8601)." },
|
|
868
|
+
text: { type: "string", maxLength: LIMITS.maxAnnotationTextLength },
|
|
869
|
+
...author,
|
|
870
|
+
createdAt: { type: "string" },
|
|
871
|
+
updatedAt: { type: "string" },
|
|
872
|
+
},
|
|
873
|
+
required: ["id", "projectId", "targetKind", "text", "createdAt", "updatedAt"],
|
|
874
|
+
},
|
|
875
|
+
GlossaryEntry: {
|
|
876
|
+
type: "object",
|
|
877
|
+
title: "Glossary entry",
|
|
878
|
+
properties: {
|
|
879
|
+
projectId: { type: "string" },
|
|
880
|
+
term: { type: "string", maxLength: LIMITS.maxGlossaryTermLength },
|
|
881
|
+
meaning: { type: "string", maxLength: LIMITS.maxGlossaryMeaningLength },
|
|
882
|
+
updatedAt: { type: "string" },
|
|
883
|
+
},
|
|
884
|
+
required: ["projectId", "term", "meaning", "updatedAt"],
|
|
885
|
+
},
|
|
886
|
+
SavedAnalysis: {
|
|
887
|
+
type: "object",
|
|
888
|
+
title: "Saved analysis",
|
|
889
|
+
properties: {
|
|
890
|
+
id: { type: "string" },
|
|
891
|
+
projectId: { type: "string" },
|
|
892
|
+
title: { type: "string", maxLength: LIMITS.maxSavedAnalysisTitleLength },
|
|
893
|
+
query: {
|
|
894
|
+
type: "object",
|
|
895
|
+
description: "The stored question, as an opaque JSON document the collector does not interpret.",
|
|
896
|
+
},
|
|
897
|
+
conclusion: {
|
|
898
|
+
type: ["string", "null"],
|
|
899
|
+
maxLength: LIMITS.maxSavedAnalysisConclusionLength,
|
|
900
|
+
},
|
|
901
|
+
...author,
|
|
902
|
+
createdAt: { type: "string" },
|
|
903
|
+
},
|
|
904
|
+
required: ["id", "projectId", "title", "query", "createdAt"],
|
|
905
|
+
},
|
|
906
|
+
PanelSpec: {
|
|
907
|
+
type: "object",
|
|
908
|
+
title: "Pinned panel",
|
|
909
|
+
properties: {
|
|
910
|
+
id: { type: "string" },
|
|
911
|
+
projectId: { type: "string" },
|
|
912
|
+
spec: {
|
|
913
|
+
...toJsonSchema(panelSpecV1Schema, "output"),
|
|
914
|
+
description: "The declarative spec the dashboard renders: a query, a chart, an optional encoding and the agent's one-line reading. Data, never code — nothing here is evaluated.",
|
|
915
|
+
},
|
|
916
|
+
...author,
|
|
917
|
+
createdAt: { type: "string" },
|
|
918
|
+
updatedAt: { type: "string" },
|
|
919
|
+
},
|
|
920
|
+
required: ["id", "projectId", "spec", "createdAt", "updatedAt"],
|
|
921
|
+
},
|
|
317
922
|
};
|
|
318
923
|
}
|
|
319
924
|
/** The reusable components: the API-key scheme, error responses, row schemas. */
|
|
@@ -325,6 +930,7 @@ function componentsFor(metrics) {
|
|
|
325
930
|
properties: { error: { type: "string", description: "Human-readable failure reason." } },
|
|
326
931
|
required: ["error"],
|
|
327
932
|
},
|
|
933
|
+
ProjectContext: contextSchema(),
|
|
328
934
|
SceneRepresentationSummary: {
|
|
329
935
|
type: "object",
|
|
330
936
|
title: "Scene representation summary",
|
|
@@ -345,6 +951,57 @@ function componentsFor(metrics) {
|
|
|
345
951
|
},
|
|
346
952
|
required: ["sceneId", "kind", "updatedAt"],
|
|
347
953
|
},
|
|
954
|
+
...metadataSchemas(),
|
|
955
|
+
Subscription: {
|
|
956
|
+
type: "object",
|
|
957
|
+
title: "Conditional subscription",
|
|
958
|
+
description: "A standing question (ADR 0051 §6): a registry metric, a window, a predicate over that window, and where a firing is delivered. A webhook secret is accepted on create and never returned — reads carry a masked placeholder.",
|
|
959
|
+
properties: {
|
|
960
|
+
id: { type: "string", description: "Collector-assigned (sub_…); absent on create." },
|
|
961
|
+
projectId: { type: "string" },
|
|
962
|
+
name: { type: "string", maxLength: 120 },
|
|
963
|
+
metric: { type: "string", description: "Registry metric id (`@uptimizr/metrics`)." },
|
|
964
|
+
filters: {
|
|
965
|
+
type: "object",
|
|
966
|
+
properties: { scene: { type: "string" } },
|
|
967
|
+
description: "Narrows the metric the subscription watches.",
|
|
968
|
+
},
|
|
969
|
+
evaluate: {
|
|
970
|
+
type: "object",
|
|
971
|
+
properties: {
|
|
972
|
+
every: { type: "string", description: "Evaluation interval, e.g. 5m. Minimum 1m." },
|
|
973
|
+
window: {
|
|
974
|
+
type: "string",
|
|
975
|
+
description: "Span each evaluation measures, e.g. 1h. Minimum 1h.",
|
|
976
|
+
},
|
|
977
|
+
bucket: { type: "string", enum: ["hour", "day"] },
|
|
978
|
+
},
|
|
979
|
+
required: ["every", "window"],
|
|
980
|
+
},
|
|
981
|
+
predicate: {
|
|
982
|
+
type: "object",
|
|
983
|
+
description: "Closed union discriminated on `kind`: threshold {column, op, value, minSample}, anomaly {sensitivity}, movers {pct, direction}, new_value {dimension}, presence {op, value}.",
|
|
984
|
+
properties: {
|
|
985
|
+
kind: {
|
|
986
|
+
type: "string",
|
|
987
|
+
enum: ["threshold", "anomaly", "movers", "new_value", "presence"],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
required: ["kind"],
|
|
991
|
+
},
|
|
992
|
+
cooldown: { type: "string", description: "Quiet period after a firing; 0s for none." },
|
|
993
|
+
delivery: {
|
|
994
|
+
type: "array",
|
|
995
|
+
description: "{ kind: sse } and/or one { kind: webhook, url, secret? }.",
|
|
996
|
+
items: { type: "object" },
|
|
997
|
+
},
|
|
998
|
+
enabled: { type: "boolean", default: true },
|
|
999
|
+
lastFiredAt: { type: ["string", "null"] },
|
|
1000
|
+
lastError: { type: ["string", "null"] },
|
|
1001
|
+
failures: { type: "integer" },
|
|
1002
|
+
},
|
|
1003
|
+
required: ["name", "metric", "evaluate", "predicate", "delivery"],
|
|
1004
|
+
},
|
|
348
1005
|
};
|
|
349
1006
|
for (const metric of metrics)
|
|
350
1007
|
schemas[metric.id] = rowSchema(metric);
|
|
@@ -355,17 +1012,25 @@ function componentsFor(metrics) {
|
|
|
355
1012
|
type: "apiKey",
|
|
356
1013
|
in: "header",
|
|
357
1014
|
name: "x-api-key",
|
|
358
|
-
description: "A project API key with the `query` capability. Reads are always scoped to the project the key resolves to; a client-supplied project id is ignored.",
|
|
1015
|
+
description: "A project API key with the `query` capability. Reads are always scoped to the project the key resolves to; a client-supplied project id is ignored. An operation whose `x-uptimizr-capability` is `query:raw` needs a key holding that capability as well, on a collector started with `ENABLE_RAW_SESSION_RETENTION` (ADR 0003 / ADR 0051 §7).",
|
|
359
1016
|
},
|
|
360
1017
|
},
|
|
361
1018
|
responses: {
|
|
362
1019
|
BadRequest: { description: "A parameter failed validation.", content: errorContent },
|
|
363
1020
|
Unauthorized: { description: "Missing or unknown API key.", content: errorContent },
|
|
364
1021
|
Forbidden: {
|
|
365
|
-
description: "The key
|
|
1022
|
+
description: "The key does not hold the capability this operation requires — an ingest-only key " +
|
|
1023
|
+
"trying to read, a `query` key asking for a `query:raw` operation, or a key without " +
|
|
1024
|
+
"`annotate` trying to write metadata. A `query:raw` operation also answers 403 when " +
|
|
1025
|
+
"the collector was started without `ENABLE_RAW_SESSION_RETENTION` (ADR 0003). Check " +
|
|
1026
|
+
"each operation's `x-uptimizr-capability`.",
|
|
1027
|
+
content: errorContent,
|
|
1028
|
+
},
|
|
1029
|
+
NotFound: { description: "No such session, scene or metadata row.", content: errorContent },
|
|
1030
|
+
Conflict: {
|
|
1031
|
+
description: "The project has reached its cap for that metadata table. Delete a row and retry.",
|
|
366
1032
|
content: errorContent,
|
|
367
1033
|
},
|
|
368
|
-
NotFound: { description: "No such session or scene.", content: errorContent },
|
|
369
1034
|
TooManyRequests: { description: "Rate limit exceeded.", content: errorContent },
|
|
370
1035
|
},
|
|
371
1036
|
schemas,
|
|
@@ -380,7 +1045,12 @@ export function buildOpenApiDocument(routeSchemas = []) {
|
|
|
380
1045
|
const byPath = new Map(routeSchemas.filter((route) => route.method === "GET").map((route) => [route.path, route]));
|
|
381
1046
|
const metrics = allMetrics();
|
|
382
1047
|
const served = metrics.filter((metric) => metric.endpoint != null);
|
|
383
|
-
const paths =
|
|
1048
|
+
const paths = {
|
|
1049
|
+
...staticPaths(),
|
|
1050
|
+
...queryDslPath(),
|
|
1051
|
+
...metadataPaths(),
|
|
1052
|
+
...panelPaths(),
|
|
1053
|
+
};
|
|
384
1054
|
for (const metric of served) {
|
|
385
1055
|
const path = toOpenApiPath(metric.endpoint.path);
|
|
386
1056
|
paths[path] ??= {};
|
|
@@ -400,8 +1070,15 @@ export function buildOpenApiDocument(routeSchemas = []) {
|
|
|
400
1070
|
"(`grain`), what its columns mean (`units`), the capture channels that must be enabled " +
|
|
401
1071
|
"for it to have data (`source-channels`), how far to trust it (`caveats`) and how to " +
|
|
402
1072
|
"read it (`interpretation`).\n\n" +
|
|
403
|
-
"The API is **read-only
|
|
404
|
-
"
|
|
1073
|
+
"The analytics API is **read-only**: no endpoint here ingests, mutates or returns " +
|
|
1074
|
+
"personal data, and nothing can write, alter or delete an event (ADR 0003, " +
|
|
1075
|
+
"ADR 0051 §9). It is aggregate-only with exactly one, doubly-gated exception — " +
|
|
1076
|
+
"`session_narrative`, a bounded compaction of one session, which requires both a " +
|
|
1077
|
+
"`query:raw` key and `ENABLE_RAW_SESSION_RETENTION`. The writable surface is the " +
|
|
1078
|
+
"`metadata` group — annotations, the glossary and saved analyses — together with " +
|
|
1079
|
+
"`panels`, the declarative specs pinned to the dashboard. Both store what a project's " +
|
|
1080
|
+
"own people and agents write, require the `annotate` capability, and are audited. " +
|
|
1081
|
+
"Check each operation's `x-uptimizr-capability`.",
|
|
405
1082
|
license: { name: "Apache-2.0", identifier: "Apache-2.0" },
|
|
406
1083
|
contact: { name: "Uptimizr", url: "https://uptimizr.com/docs/" },
|
|
407
1084
|
},
|
|
@@ -413,6 +1090,18 @@ export function buildOpenApiDocument(routeSchemas = []) {
|
|
|
413
1090
|
description: CATEGORY_DESCRIPTIONS[category] ?? category,
|
|
414
1091
|
})),
|
|
415
1092
|
{ name: "meta", description: "Self-description and liveness." },
|
|
1093
|
+
{
|
|
1094
|
+
name: "metadata",
|
|
1095
|
+
description: "What people and agents leave behind: annotations, the project glossary, saved analyses. Reads need `query`; every write needs `annotate` and is audited.",
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: "panels",
|
|
1099
|
+
description: "Declarative panel specs an agent pins to the dashboard (ADR 0051 §7): a query, a chart and a reading, stored as data and rendered with the panels the dashboard already ships — never as loaded code. Reads need `query`; every write needs `annotate` and is audited.",
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: "subscriptions",
|
|
1103
|
+
description: "Conditional subscriptions: standing predicates over a metric, delivered over SSE and signed webhooks (ADR 0051 §6).",
|
|
1104
|
+
},
|
|
416
1105
|
],
|
|
417
1106
|
paths,
|
|
418
1107
|
components: componentsFor(served),
|