@uptimizr/db-postgres 1.0.2 → 2.0.1
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 +115 -0
- package/README.md +3 -1
- package/dist/audit.d.ts +12 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +73 -0
- package/dist/audit.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +67 -0
- package/dist/migrations.js.map +1 -1
- package/dist/projects.d.ts +9 -5
- package/dist/projects.d.ts.map +1 -1
- package/dist/projects.js +53 -23
- package/dist/projects.js.map +1 -1
- package/dist/queries.d.ts +6 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +9 -2
- package/dist/queries.js.map +1 -1
- package/dist/sceneRegions.d.ts +18 -0
- package/dist/sceneRegions.d.ts.map +1 -0
- package/dist/sceneRegions.js +64 -0
- package/dist/sceneRegions.js.map +1 -0
- package/llms.txt +31 -0
- package/package.json +6 -4
package/AGENTS.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# AGENTS.md — @uptimizr/db-postgres
|
|
2
|
+
|
|
3
|
+
> Packaged agent guide. For the human reference see [README.md](./README.md); for design
|
|
4
|
+
> rationale see the project ADRs at https://github.com/RaananW/Uptimizr/tree/main/docs/adr.
|
|
5
|
+
|
|
6
|
+
## What this package is
|
|
7
|
+
|
|
8
|
+
The optional **single-tenant PostgreSQL store** for the Uptimizr collector — for self-hosters who
|
|
9
|
+
already run Postgres and want a familiar, **multi-writer** relational backend instead of the
|
|
10
|
+
default single-file DuckDB store (ADR 0020).
|
|
11
|
+
|
|
12
|
+
It is a **re-home + dialect emitter, not a rewrite**. Every analytics aggregation is authored once
|
|
13
|
+
in [`@uptimizr/db`](../db) against the dialect-agnostic query layer
|
|
14
|
+
(`buildX(projectId, opts, dialect)` → `QuerySpec`) and rendered to Postgres SQL with the shared
|
|
15
|
+
`postgresDialect` (also
|
|
16
|
+
exported from `@uptimizr/db`). This package adds a pooled [`pg`](https://node-postgres.com)
|
|
17
|
+
client, forward-only migrations, and metadata helpers that satisfy the same `CollectorStore`
|
|
18
|
+
contract as DuckDB and ClickHouse.
|
|
19
|
+
|
|
20
|
+
Server/Node only — no DOM imports. Single-tenant only: no `org_id`, no tenant isolation.
|
|
21
|
+
|
|
22
|
+
## Install / select it
|
|
23
|
+
|
|
24
|
+
The collector picks a store with `COLLECTOR_STORE`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
COLLECTOR_STORE=postgres \
|
|
28
|
+
POSTGRES_URL=postgresql://uptimizr:uptimizr@localhost:5432/uptimizr \
|
|
29
|
+
npx -p @uptimizr/collector-server uptimizr serve
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Variable | Purpose |
|
|
33
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------- |
|
|
34
|
+
| `POSTGRES_URL` (or `DATABASE_URL`) | libpq connection URI (`?sslmode=require` etc. apply). The database must already exist. |
|
|
35
|
+
| `POSTGRES_SCHEMA` | Schema the store's tables live in (default `public`; created on first boot). |
|
|
36
|
+
| `POSTGRES_POOL_MAX` | Maximum pooled connections per collector process (default `10`). |
|
|
37
|
+
|
|
38
|
+
Migrations run on store creation — idempotent, forward-only (ADR 0007) and serialized behind an
|
|
39
|
+
advisory lock, so several collector instances may boot concurrently against one database.
|
|
40
|
+
Postgres 14+ is supported (tested against 16).
|
|
41
|
+
|
|
42
|
+
`uptimizr init` / `new-project` / `new-key` / `migrate` / `regions` all honour `COLLECTOR_STORE`,
|
|
43
|
+
so export the store + connection variables before running them and the project you mint lives in
|
|
44
|
+
Postgres. A local Postgres is available from [`infra/docker`](../../../infra/docker)
|
|
45
|
+
(`pnpm stack:up`).
|
|
46
|
+
|
|
47
|
+
## Canonical usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import {
|
|
51
|
+
createPostgresClient,
|
|
52
|
+
migratePostgres,
|
|
53
|
+
insertEvents,
|
|
54
|
+
getSessionEvents,
|
|
55
|
+
resolveApiKey,
|
|
56
|
+
runPostgresQuery,
|
|
57
|
+
} from "@uptimizr/db-postgres";
|
|
58
|
+
import { buildPointerHeatmap, postgresDialect, type HeatmapBinRow } from "@uptimizr/db";
|
|
59
|
+
|
|
60
|
+
const db = createPostgresClient(settings);
|
|
61
|
+
await migratePostgres(db);
|
|
62
|
+
|
|
63
|
+
await insertEvents(db, events); // validated upstream at the collector boundary
|
|
64
|
+
const heat = await runPostgresQuery<HeatmapBinRow>(
|
|
65
|
+
db,
|
|
66
|
+
buildPointerHeatmap("project-id", { bins: 50 }, postgresDialect),
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `CollectorStore` itself is assembled from these building blocks in the collector server
|
|
71
|
+
(`oss/apps/collector-server/src/postgresStore.ts`, `createPostgresStore`) — this package stays a
|
|
72
|
+
store-agnostic toolkit.
|
|
73
|
+
|
|
74
|
+
## Rules for agents
|
|
75
|
+
|
|
76
|
+
- **Never author an aggregation here.** A new aggregation is a pure
|
|
77
|
+
`buildX(projectId, opts, dialect)` builder in `@uptimizr/db` plus a `@uptimizr/metrics` registry
|
|
78
|
+
entry. This package only renders and runs.
|
|
79
|
+
- **Migrations are forward-only, additive and idempotent** (ADR 0007). Append to
|
|
80
|
+
`POSTGRES_MIGRATIONS`; never edit a shipped migration. Keep the advisory lock so concurrent
|
|
81
|
+
collector boots stay safe.
|
|
82
|
+
- `runPostgresQuery` must call `coerceRows(spec.metric, rows)` at the one point rows leave the
|
|
83
|
+
driver (ADR 0051 §2) — `pg` hands back `int8` / `numeric` as strings. `null` stays `null`: an
|
|
84
|
+
aggregate over an empty set is "no samples", not `0`.
|
|
85
|
+
- Postgres has no `ASOF JOIN` and no MergeTree rollups; both are already closed at the **shared**
|
|
86
|
+
layer (`renderNearestRowJoin` and plain query-time `perf_daily` / `events_daily` views). Reuse
|
|
87
|
+
those helpers rather than hand-writing a per-query workaround.
|
|
88
|
+
- Validate events upstream at the collector boundary; this layer assumes valid input.
|
|
89
|
+
- API keys are only ever stored as **SHA-256 hashes** — never persist a raw key. Read capability
|
|
90
|
+
sets with `parseApiKeyCapabilities` and write them with `toApiKeyColumns` (from `@uptimizr/db`)
|
|
91
|
+
so ordering, validation and the per-key rate-limit columns stay consistent across engines.
|
|
92
|
+
- **The audit log records key ids, never keys.** Serialize parameters with `serializeAuditParams`
|
|
93
|
+
and clamp the endpoint with `clampAuditTool` before they reach `recordAudit`.
|
|
94
|
+
- Privacy (ADR 0003): no raw IPs, no PII. Raw per-session reads are gated by the collector's
|
|
95
|
+
`ENABLE_RAW_SESSION_RETENTION` **and** a `query:raw` capability — do not add a bypass here.
|
|
96
|
+
- **Parity is the contract.** The shared `PARITY_EVENTS` / `PARITY_CASES` / `diffParity` harness in
|
|
97
|
+
`@uptimizr/db` proves every engine returns equal analytics. Extend the fixtures and golden when
|
|
98
|
+
you add an aggregation or event type, and document any genuine divergence.
|
|
99
|
+
- Identifiers interpolated into DDL go through `assertSafeIdentifier` — never string-concatenate a
|
|
100
|
+
schema or table name from input.
|
|
101
|
+
|
|
102
|
+
## Tests and parity
|
|
103
|
+
|
|
104
|
+
`dialect.test.ts` is a pure unit suite (no server). `postgresParity.test.ts` /
|
|
105
|
+
`postgresStore.test.ts` probe `POSTGRES_URL` (or `DATABASE_URL`) first and **skip when no server is
|
|
106
|
+
reachable**, so the default `pnpm test` stays Docker-free; they use throwaway schemas dropped on
|
|
107
|
+
teardown. Set **`POSTGRES_PARITY_REQUIRED=1`** to fail instead of skipping — that is what the
|
|
108
|
+
"Store parity (Postgres)" CI job does (`pnpm test:parity:postgres` runs them locally with `.env`
|
|
109
|
+
loaded).
|
|
110
|
+
|
|
111
|
+
## More
|
|
112
|
+
|
|
113
|
+
- Package reference: [README.md](./README.md)
|
|
114
|
+
- Storage contracts + dialect layer: [`@uptimizr/db`](../db/AGENTS.md)
|
|
115
|
+
- Collector configuration: https://github.com/RaananW/Uptimizr/blob/main/docs/integration.md
|
package/README.md
CHANGED
|
@@ -98,8 +98,10 @@ selectPrefix: "SELECT TOP 1", selectSuffix: "" }`.
|
|
|
98
98
|
| `client.ts` | Pooled `pg` wrapper (UTC session, schema search path, plain-JS values). |
|
|
99
99
|
| `migrations.ts` | Forward-only DDL (events, node_samples, metadata, query-time views). |
|
|
100
100
|
| `events.ts` | Batched multi-row inserts + replay-complete session reads. |
|
|
101
|
-
| `projects.ts` | Project + API-key metadata (SHA-256 hashes).
|
|
101
|
+
| `projects.ts` | Project + API-key metadata (SHA-256 hashes, capability sets). |
|
|
102
|
+
| `audit.ts` | Agent audit log: record / list / prune (`agent_audit`). |
|
|
102
103
|
| `sceneRegistry.ts` | Per-`(project, scene)` representation upserts/reads (`ON CONFLICT`). |
|
|
104
|
+
| `sceneRegions.ts` | Named scene regions: replace-the-set writes in one transaction. |
|
|
103
105
|
| `queries.ts` | `runPostgresQuery` — executes a rendered `QuerySpec`. |
|
|
104
106
|
|
|
105
107
|
The `CollectorStore` itself is assembled from these building blocks in the
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AgentAuditEntry, type AgentAuditInput, type AuditQueryOptions } from "@uptimizr/db";
|
|
2
|
+
import type { PostgresClient } from "./client.js";
|
|
3
|
+
/** Append one audit row. */
|
|
4
|
+
export declare function recordAudit(client: PostgresClient, entry: AgentAuditInput): Promise<void>;
|
|
5
|
+
/** Read a project's audit rows, newest first, within an optional time range. */
|
|
6
|
+
export declare function listAudit(client: PostgresClient, projectId: string, opts?: AuditQueryOptions): Promise<AgentAuditEntry[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Delete audit rows older than `cutoffMs` (epoch ms). Idempotent, so the
|
|
9
|
+
* collector can run it on a timer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pruneAudit(client: PostgresClient, cutoffMs: number): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EAEvB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0ClD,4BAA4B;AAC5B,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB/F;AAED,gFAAgF;AAChF,wBAAsB,SAAS,CAC7B,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,eAAe,EAAE,CAAC,CAsB5B;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF"}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { clampAuditTool, } from "@uptimizr/db";
|
|
3
|
+
/**
|
|
4
|
+
* Agent audit log for the single-tenant Postgres store (#309, ADR 0051 §7).
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the DuckDB accessors column-for-column. One row per authenticated
|
|
7
|
+
* request made with a non-dashboard API key; `params` is redacted and bounded by
|
|
8
|
+
* the caller before it arrives, and the subject is the key's **id**, never the
|
|
9
|
+
* key or its hash (ADR 0003).
|
|
10
|
+
*/
|
|
11
|
+
/** `timestamp` → epoch milliseconds. */
|
|
12
|
+
const AT_MS = `(EXTRACT(EPOCH FROM at) * 1000)::bigint`;
|
|
13
|
+
function toEntry(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
projectId: row.project_id,
|
|
17
|
+
keyId: row.key_id,
|
|
18
|
+
at: new Date(Number(row.at_ms)),
|
|
19
|
+
surface: row.surface,
|
|
20
|
+
toolOrPath: row.tool_or_path,
|
|
21
|
+
params: row.params,
|
|
22
|
+
rowCount: row.row_count == null ? null : Number(row.row_count),
|
|
23
|
+
durationMs: Number(row.duration_ms),
|
|
24
|
+
status: Number(row.status),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Append one audit row. */
|
|
28
|
+
export async function recordAudit(client, entry) {
|
|
29
|
+
await client.query(`INSERT INTO agent_audit (id, project_id, key_id, at, surface, tool_or_path, params,
|
|
30
|
+
row_count, duration_ms, status)
|
|
31
|
+
VALUES ($1, $2, $3, to_timestamp($4::double precision / 1000) AT TIME ZONE 'utc',
|
|
32
|
+
$5, $6, $7, $8, $9, $10)`, [
|
|
33
|
+
randomUUID(),
|
|
34
|
+
entry.projectId,
|
|
35
|
+
entry.keyId,
|
|
36
|
+
entry.at?.getTime() ?? Date.now(),
|
|
37
|
+
entry.surface,
|
|
38
|
+
clampAuditTool(entry.toolOrPath),
|
|
39
|
+
entry.params,
|
|
40
|
+
entry.rowCount ?? null,
|
|
41
|
+
Math.trunc(entry.durationMs),
|
|
42
|
+
Math.trunc(entry.status),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
/** Read a project's audit rows, newest first, within an optional time range. */
|
|
46
|
+
export async function listAudit(client, projectId, opts = {}) {
|
|
47
|
+
const limit = Math.min(Math.max(Math.trunc(opts.limit ?? 100), 1), 1000);
|
|
48
|
+
const params = [projectId];
|
|
49
|
+
const where = ["project_id = $1"];
|
|
50
|
+
if (opts.since != null) {
|
|
51
|
+
params.push(Math.trunc(opts.since));
|
|
52
|
+
where.push(`at >= to_timestamp($${params.length}::double precision / 1000) AT TIME ZONE 'utc'`);
|
|
53
|
+
}
|
|
54
|
+
if (opts.until != null) {
|
|
55
|
+
params.push(Math.trunc(opts.until));
|
|
56
|
+
where.push(`at < to_timestamp($${params.length}::double precision / 1000) AT TIME ZONE 'utc'`);
|
|
57
|
+
}
|
|
58
|
+
const rows = await client.query(`SELECT id, project_id, key_id, ${AT_MS} AS at_ms, surface, tool_or_path, params,
|
|
59
|
+
row_count, duration_ms, status
|
|
60
|
+
FROM agent_audit
|
|
61
|
+
WHERE ${where.join(" AND ")}
|
|
62
|
+
ORDER BY at DESC
|
|
63
|
+
LIMIT ${limit}`, params);
|
|
64
|
+
return rows.map(toEntry);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Delete audit rows older than `cutoffMs` (epoch ms). Idempotent, so the
|
|
68
|
+
* collector can run it on a timer.
|
|
69
|
+
*/
|
|
70
|
+
export async function pruneAudit(client, cutoffMs) {
|
|
71
|
+
await client.query(`DELETE FROM agent_audit WHERE at < to_timestamp($1::double precision / 1000) AT TIME ZONE 'utc'`, [Math.trunc(cutoffMs)]);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export { insertEvents, getSessionEvents, streamSessionEvents, getSessionMeta } f
|
|
|
18
18
|
export type { SessionMeta } from "./events.js";
|
|
19
19
|
export { createProject, getProject, createApiKey, resolveApiKey, hashApiKey, apiKeyPrefix, generateApiKey, } from "./projects.js";
|
|
20
20
|
export type { Project, ApiKeyRecord } from "./projects.js";
|
|
21
|
+
export { recordAudit, listAudit, pruneAudit } from "./audit.js";
|
|
21
22
|
export { upsertSceneProxy, getSceneRepresentation, listSceneRepresentations, } from "./sceneRegistry.js";
|
|
22
23
|
export type { SceneRepresentation, SceneRepresentationKind, SceneRepresentationSummary, } from "./sceneRegistry.js";
|
|
24
|
+
export { putSceneRegions, getSceneRegions, listSceneRegions } from "./sceneRegions.js";
|
|
25
|
+
export type { SceneRegionRecord, SceneRegionSummary } from "./sceneRegions.js";
|
|
23
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvF,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,7 @@ export { POSTGRES_MIGRATIONS, migratePostgres } from "./migrations.js";
|
|
|
15
15
|
export { runPostgresQuery } from "./queries.js";
|
|
16
16
|
export { insertEvents, getSessionEvents, streamSessionEvents, getSessionMeta } from "./events.js";
|
|
17
17
|
export { createProject, getProject, createApiKey, resolveApiKey, hashApiKey, apiKeyPrefix, generateApiKey, } from "./projects.js";
|
|
18
|
+
export { recordAudit, listAudit, pruneAudit } from "./audit.js";
|
|
18
19
|
export { upsertSceneProxy, getSceneRepresentation, listSceneRepresentations, } from "./sceneRegistry.js";
|
|
20
|
+
export { putSceneRegions, getSceneRegions, listSceneRegions } from "./sceneRegions.js";
|
|
19
21
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlG,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlG,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/migrations.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CA0P1E,CAAC;AASF;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
package/dist/migrations.js
CHANGED
|
@@ -213,6 +213,73 @@ export const POSTGRES_MIGRATIONS = [
|
|
|
213
213
|
GROUP BY project_id, event_type, CAST(ts AS DATE);
|
|
214
214
|
`,
|
|
215
215
|
},
|
|
216
|
+
// --- Agent-scoped keys (#309, ADR 0051 §7) --------------------------------
|
|
217
|
+
// The singular `capability` column becomes a capability *set*, stored as a
|
|
218
|
+
// canonical comma-separated token list in a plain `text` column (mirrors the
|
|
219
|
+
// DuckDB store, so no engine needs a JSON type). Forward-only and additive:
|
|
220
|
+
// `0007_api_keys` is untouched and its column keeps feeding the read path as
|
|
221
|
+
// the fallback for any row this backfill has not reached.
|
|
222
|
+
{
|
|
223
|
+
id: "0011_api_keys_capabilities",
|
|
224
|
+
sql: /* sql */ `
|
|
225
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS capabilities text;
|
|
226
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS label text;
|
|
227
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS rate_limit_max bigint;
|
|
228
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS rate_limit_window_ms bigint;
|
|
229
|
+
`,
|
|
230
|
+
},
|
|
231
|
+
// Idempotent backfill: promote each legacy single capability to a one-element
|
|
232
|
+
// set, exactly once. Guarded on NULL/'' so re-running on every boot is a no-op
|
|
233
|
+
// after the first (and never clobbers a key minted with a set).
|
|
234
|
+
{
|
|
235
|
+
id: "0012_api_keys_capabilities_backfill",
|
|
236
|
+
sql: /* sql */ `
|
|
237
|
+
UPDATE api_keys
|
|
238
|
+
SET capabilities = coalesce(nullif(capability, ''), 'query')
|
|
239
|
+
WHERE capabilities IS NULL OR capabilities = '';
|
|
240
|
+
`,
|
|
241
|
+
},
|
|
242
|
+
// Agent audit log: one row per authenticated request made with a non-dashboard
|
|
243
|
+
// key. `params` is bounded and redacted before it is written (never the key);
|
|
244
|
+
// rows expire after AUDIT_RETENTION_DAYS. Metadata, not events.
|
|
245
|
+
{
|
|
246
|
+
id: "0013_agent_audit",
|
|
247
|
+
sql: /* sql */ `
|
|
248
|
+
CREATE TABLE IF NOT EXISTS agent_audit (
|
|
249
|
+
id text PRIMARY KEY,
|
|
250
|
+
project_id text NOT NULL,
|
|
251
|
+
key_id text NOT NULL,
|
|
252
|
+
at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
|
|
253
|
+
surface text NOT NULL DEFAULT 'http',
|
|
254
|
+
tool_or_path text NOT NULL,
|
|
255
|
+
params text NOT NULL DEFAULT '',
|
|
256
|
+
row_count bigint,
|
|
257
|
+
duration_ms bigint NOT NULL DEFAULT 0,
|
|
258
|
+
status integer NOT NULL DEFAULT 200
|
|
259
|
+
);
|
|
260
|
+
CREATE INDEX IF NOT EXISTS agent_audit_project_at_idx ON agent_audit (project_id, at DESC);
|
|
261
|
+
`,
|
|
262
|
+
},
|
|
263
|
+
// Scene regions (ADR 0051 §2 / sketch §B.2): developer-named, labelled boxes
|
|
264
|
+
// that extend the scene registry (ADR 0014) with a vocabulary for *where*.
|
|
265
|
+
// One row per region, keyed by (project, scene, region); regions may overlap.
|
|
266
|
+
// `bounds` is JSON text (the `[minX,…,maxZ]` tuple) parsed by the row mapper,
|
|
267
|
+
// exactly as `scene_representations.bounds` is.
|
|
268
|
+
{
|
|
269
|
+
id: "0014_scene_regions",
|
|
270
|
+
sql: /* sql */ `
|
|
271
|
+
CREATE TABLE IF NOT EXISTS scene_regions (
|
|
272
|
+
project_id text NOT NULL,
|
|
273
|
+
scene_id text NOT NULL,
|
|
274
|
+
region_id text NOT NULL,
|
|
275
|
+
label text NOT NULL,
|
|
276
|
+
description text,
|
|
277
|
+
bounds text NOT NULL,
|
|
278
|
+
updated_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
|
|
279
|
+
PRIMARY KEY (project_id, scene_id, region_id)
|
|
280
|
+
);
|
|
281
|
+
`,
|
|
282
|
+
},
|
|
216
283
|
];
|
|
217
284
|
/**
|
|
218
285
|
* Stable advisory-lock key that serializes concurrent boots of several
|
package/dist/migrations.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAA+C;IAC7E,6EAA6E;IAC7E;QACE,EAAE,EAAE,aAAa;QACjB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2Cd;KACF;IACD;QACE,EAAE,EAAE,yBAAyB;QAC7B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,yEAAyE;IACzE,wEAAwE;IACxE,mDAAmD;IACnD;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,uEAAuE;IACvE,gEAAgE;IAChE;QACE,EAAE,EAAE,mBAAmB;QACvB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;KAed;KACF;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,8EAA8E;IAC9E;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;KAMd;KACF;IACD,wEAAwE;IACxE,wEAAwE;IACxE;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;KAUd;KACF;IACD,8EAA8E;IAC9E,4EAA4E;IAC5E,iEAAiE;IACjE;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;KAiBd;KACF;IACD,6EAA6E;IAC7E,+DAA+D;IAC/D,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE;QACE,EAAE,EAAE,sBAAsB;QAC1B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;KAYd;KACF;IACD;QACE,EAAE,EAAE,wBAAwB;QAC5B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;KASd;KACF;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,SAAS;AAEjD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,QAA0B;IAE1B,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,EAAE,CAAC,OAAO,CAAC,gCAAgC,kBAAkB,GAAG,CAAC,CAAC;QACxE,MAAM,EAAE,CAAC,OAAO,CAAC,+BAA+B,MAAM,EAAE,CAAC,CAAC;QAC1D,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE,CAAC;YAC5C,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAA+C;IAC7E,6EAA6E;IAC7E;QACE,EAAE,EAAE,aAAa;QACjB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2Cd;KACF;IACD;QACE,EAAE,EAAE,yBAAyB;QAC7B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,yEAAyE;IACzE,wEAAwE;IACxE,mDAAmD;IACnD;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,uEAAuE;IACvE,gEAAgE;IAChE;QACE,EAAE,EAAE,mBAAmB;QACvB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;KAed;KACF;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,8EAA8E;IAC9E;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;KAMd;KACF;IACD,wEAAwE;IACxE,wEAAwE;IACxE;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;KAUd;KACF;IACD,8EAA8E;IAC9E,4EAA4E;IAC5E,iEAAiE;IACjE;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;KAiBd;KACF;IACD,6EAA6E;IAC7E,+DAA+D;IAC/D,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE;QACE,EAAE,EAAE,sBAAsB;QAC1B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;KAYd;KACF;IACD;QACE,EAAE,EAAE,wBAAwB;QAC5B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;KASd;KACF;IACD,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,4EAA4E;IAC5E,6EAA6E;IAC7E,0DAA0D;IAC1D;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;;;KAKd;KACF;IACD,8EAA8E;IAC9E,+EAA+E;IAC/E,gEAAgE;IAChE;QACE,EAAE,EAAE,qCAAqC;QACzC,GAAG,EAAE,SAAS,CAAC;;;;KAId;KACF;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,gEAAgE;IAChE;QACE,EAAE,EAAE,kBAAkB;QACtB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;KAcd;KACF;IACD,6EAA6E;IAC7E,2EAA2E;IAC3E,8EAA8E;IAC9E,8EAA8E;IAC9E,gDAAgD;IAChD;QACE,EAAE,EAAE,oBAAoB;QACxB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;KAWd;KACF;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,SAAS;AAEjD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,QAA0B;IAE1B,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,EAAE,CAAC,OAAO,CAAC,gCAAgC,kBAAkB,GAAG,CAAC,CAAC;QACxE,MAAM,EAAE,CAAC,OAAO,CAAC,+BAA+B,MAAM,EAAE,CAAC,CAAC;QAC1D,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE,CAAC;YAC5C,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/projects.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiKeyPrefix, generateApiKey, hashApiKey, type
|
|
1
|
+
import { apiKeyPrefix, generateApiKey, hashApiKey, type ApiKeyRecord, type CreateApiKeyOptions, type Project, type ResolvedApiKey } from "@uptimizr/db";
|
|
2
2
|
import type { PostgresClient } from "./client.js";
|
|
3
3
|
export type { Project, ApiKeyRecord };
|
|
4
4
|
export { hashApiKey, apiKeyPrefix, generateApiKey };
|
|
@@ -9,15 +9,19 @@ export declare function getProject(client: PostgresClient, id: string): Promise<
|
|
|
9
9
|
/**
|
|
10
10
|
* Issue a new API key for a project. Returns both the record and the plaintext
|
|
11
11
|
* key — the plaintext is shown to the caller exactly once and never stored.
|
|
12
|
+
*
|
|
13
|
+
* The legacy singular `capability` column is written alongside the set (its
|
|
14
|
+
* first token) so a collector still running older code keeps resolving the key.
|
|
12
15
|
*/
|
|
13
|
-
export declare function createApiKey(client: PostgresClient, projectId: string,
|
|
16
|
+
export declare function createApiKey(client: PostgresClient, projectId: string, options?: CreateApiKeyOptions): Promise<{
|
|
14
17
|
key: string;
|
|
15
18
|
record: ApiKeyRecord;
|
|
16
19
|
}>;
|
|
17
20
|
/**
|
|
18
|
-
* Resolve a plaintext API key to its (non-revoked) project id
|
|
19
|
-
* `null` when the key is unknown or
|
|
20
|
-
* authenticate and scope
|
|
21
|
+
* Resolve a plaintext API key to its (non-revoked) project id, key id,
|
|
22
|
+
* capability set and per-key rate limit, or `null` when the key is unknown or
|
|
23
|
+
* revoked. The collector uses this to authenticate and scope requests at the
|
|
24
|
+
* boundary, and to attribute audit rows to a key without ever logging the key.
|
|
21
25
|
*/
|
|
22
26
|
export declare function resolveApiKey(client: PostgresClient, plaintext: string): Promise<ResolvedApiKey | null>;
|
|
23
27
|
//# sourceMappingURL=projects.d.ts.map
|
package/dist/projects.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,EAIV,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;AAwBpD,sCAAsC;AACtC,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO1F;AAED,6DAA6D;AAC7D,wBAAsB,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAO5F;AAqCD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,CAAC,CAqBhD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAehC"}
|
package/dist/projects.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { apiKeyPrefix, generateApiKey, hashApiKey, } from "@uptimizr/db";
|
|
2
|
+
import { apiKeyPrefix, generateApiKey, hashApiKey, parseApiKeyCapabilities, toApiKeyColumns, toApiKeyRateLimit, } from "@uptimizr/db";
|
|
3
3
|
export { hashApiKey, apiKeyPrefix, generateApiKey };
|
|
4
4
|
/**
|
|
5
5
|
* Project + API-key metadata for the single-tenant Postgres store (ADR 0020).
|
|
@@ -26,37 +26,67 @@ export async function getProject(client, id) {
|
|
|
26
26
|
const row = rows[0];
|
|
27
27
|
return row ? toProject(row) : null;
|
|
28
28
|
}
|
|
29
|
+
/** Columns every API-key read selects, mapped by {@link toApiKeyRecord}. */
|
|
30
|
+
const API_KEY_COLS = `id, project_id, key_prefix, ${EPOCH_MS("created_at")} AS created_at_ms,
|
|
31
|
+
${EPOCH_MS("revoked_at")} AS revoked_at_ms, capability, capabilities, label,
|
|
32
|
+
rate_limit_max, rate_limit_window_ms`;
|
|
33
|
+
function toApiKeyRecord(row) {
|
|
34
|
+
return {
|
|
35
|
+
id: row.id,
|
|
36
|
+
projectId: row.project_id,
|
|
37
|
+
keyPrefix: row.key_prefix,
|
|
38
|
+
createdAt: new Date(row.created_at_ms),
|
|
39
|
+
revokedAt: row.revoked_at_ms == null ? null : new Date(row.revoked_at_ms),
|
|
40
|
+
capabilities: parseApiKeyCapabilities(row.capabilities, row.capability),
|
|
41
|
+
label: row.label ?? null,
|
|
42
|
+
// `bigint` arrives as a string from node-postgres; `toApiKeyRateLimit` coerces.
|
|
43
|
+
rateLimit: toApiKeyRateLimit(row.rate_limit_max == null ? null : Number(row.rate_limit_max), row.rate_limit_window_ms == null ? null : Number(row.rate_limit_window_ms)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
29
46
|
/**
|
|
30
47
|
* Issue a new API key for a project. Returns both the record and the plaintext
|
|
31
48
|
* key — the plaintext is shown to the caller exactly once and never stored.
|
|
49
|
+
*
|
|
50
|
+
* The legacy singular `capability` column is written alongside the set (its
|
|
51
|
+
* first token) so a collector still running older code keeps resolving the key.
|
|
32
52
|
*/
|
|
33
|
-
export async function createApiKey(client, projectId,
|
|
53
|
+
export async function createApiKey(client, projectId, options = {}) {
|
|
34
54
|
const key = generateApiKey();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
55
|
+
const cols = toApiKeyColumns(options);
|
|
56
|
+
const rows = await client.query(`INSERT INTO api_keys (id, project_id, key_hash, key_prefix, capability, capabilities,
|
|
57
|
+
label, rate_limit_max, rate_limit_window_ms)
|
|
58
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
59
|
+
RETURNING ${API_KEY_COLS}`, [
|
|
60
|
+
randomUUID(),
|
|
61
|
+
projectId,
|
|
62
|
+
hashApiKey(key),
|
|
63
|
+
apiKeyPrefix(key),
|
|
64
|
+
cols.capabilities.split(",")[0],
|
|
65
|
+
cols.capabilities,
|
|
66
|
+
cols.label,
|
|
67
|
+
cols.rateLimitMax,
|
|
68
|
+
cols.rateLimitWindowMs,
|
|
69
|
+
]);
|
|
70
|
+
return { key, record: toApiKeyRecord(rows[0]) };
|
|
51
71
|
}
|
|
52
72
|
/**
|
|
53
|
-
* Resolve a plaintext API key to its (non-revoked) project id
|
|
54
|
-
* `null` when the key is unknown or
|
|
55
|
-
* authenticate and scope
|
|
73
|
+
* Resolve a plaintext API key to its (non-revoked) project id, key id,
|
|
74
|
+
* capability set and per-key rate limit, or `null` when the key is unknown or
|
|
75
|
+
* revoked. The collector uses this to authenticate and scope requests at the
|
|
76
|
+
* boundary, and to attribute audit rows to a key without ever logging the key.
|
|
56
77
|
*/
|
|
57
78
|
export async function resolveApiKey(client, plaintext) {
|
|
58
|
-
const rows = await client.query(`SELECT
|
|
79
|
+
const rows = await client.query(`SELECT ${API_KEY_COLS} FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL`, [hashApiKey(plaintext)]);
|
|
59
80
|
const row = rows[0];
|
|
60
|
-
|
|
81
|
+
if (!row)
|
|
82
|
+
return null;
|
|
83
|
+
const record = toApiKeyRecord(row);
|
|
84
|
+
return {
|
|
85
|
+
projectId: record.projectId,
|
|
86
|
+
keyId: record.id,
|
|
87
|
+
capabilities: record.capabilities,
|
|
88
|
+
label: record.label,
|
|
89
|
+
rateLimit: record.rateLimit,
|
|
90
|
+
};
|
|
61
91
|
}
|
|
62
92
|
//# sourceMappingURL=projects.js.map
|
package/dist/projects.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"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"}
|
package/dist/queries.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ import type { PostgresClient } from "./client.js";
|
|
|
8
8
|
* reuses with `@p1…`). The client's type parsers return plain-JS values
|
|
9
9
|
* (64-bit integers and numerics as numbers, arrays as arrays), so rows match the
|
|
10
10
|
* shapes produced by the DuckDB `runDuckdbQuery`.
|
|
11
|
+
*
|
|
12
|
+
* This is the single point where rows leave the `pg` driver, so it is where
|
|
13
|
+
* {@link coerceRows} runs (ADR 0051 §2): `pg` hands back `int8`/`numeric` as
|
|
14
|
+
* strings unless a type parser is registered for the OID, so the guarantee that a
|
|
15
|
+
* numeric column *is* a number belongs to the store, not to a parser
|
|
16
|
+
* registration that a future column type could sidestep.
|
|
11
17
|
*/
|
|
12
18
|
export declare function runPostgresQuery<T>(client: PostgresClient, spec: QuerySpec): Promise<T[]>;
|
|
13
19
|
//# sourceMappingURL=queries.d.ts.map
|
package/dist/queries.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAI/F"}
|
package/dist/queries.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toPositionalParams } from "@uptimizr/db";
|
|
1
|
+
import { coerceRows, toPositionalParams } from "@uptimizr/db";
|
|
2
2
|
/**
|
|
3
3
|
* Execute a dialect-agnostic {@link QuerySpec} (rendered with `postgresDialect`)
|
|
4
4
|
* against the Postgres store and return typed rows. The dialect emits named
|
|
@@ -7,9 +7,16 @@ import { toPositionalParams } from "@uptimizr/db";
|
|
|
7
7
|
* reuses with `@p1…`). The client's type parsers return plain-JS values
|
|
8
8
|
* (64-bit integers and numerics as numbers, arrays as arrays), so rows match the
|
|
9
9
|
* shapes produced by the DuckDB `runDuckdbQuery`.
|
|
10
|
+
*
|
|
11
|
+
* This is the single point where rows leave the `pg` driver, so it is where
|
|
12
|
+
* {@link coerceRows} runs (ADR 0051 §2): `pg` hands back `int8`/`numeric` as
|
|
13
|
+
* strings unless a type parser is registered for the OID, so the guarantee that a
|
|
14
|
+
* numeric column *is* a number belongs to the store, not to a parser
|
|
15
|
+
* registration that a future column type could sidestep.
|
|
10
16
|
*/
|
|
11
17
|
export async function runPostgresQuery(client, spec) {
|
|
12
18
|
const { sql, values } = toPositionalParams(spec.query, spec.query_params, (i) => `$${i}`);
|
|
13
|
-
|
|
19
|
+
const rows = await client.query(sql, values);
|
|
20
|
+
return coerceRows(spec.metric, rows);
|
|
14
21
|
}
|
|
15
22
|
//# sourceMappingURL=queries.js.map
|
package/dist/queries.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAkB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAkB,MAAM,cAAc,CAAC;AAG9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,MAAsB,EAAE,IAAe;IAC/E,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1F,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAI,GAAG,EAAE,MAAM,CAAC,CAAC;IAChD,OAAO,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SceneRegionRecord, SceneRegionSummary } from "@uptimizr/db";
|
|
2
|
+
import type { SceneRegion } from "@uptimizr/schema";
|
|
3
|
+
import type { PostgresClient } from "./client.js";
|
|
4
|
+
export type { SceneRegionRecord, SceneRegionSummary };
|
|
5
|
+
/**
|
|
6
|
+
* Replace the whole region set of `(projectId, sceneId)` with `regions` and
|
|
7
|
+
* return the stored rows. The delete + inserts run in one transaction, so a
|
|
8
|
+
* concurrent reader never sees a half-replaced set.
|
|
9
|
+
*/
|
|
10
|
+
export declare function putSceneRegions(client: PostgresClient, projectId: string, sceneId: string, regions: readonly SceneRegion[]): Promise<SceneRegionRecord[]>;
|
|
11
|
+
/** Read one scene's regions, ordered by region id (stable for callers/diffs). */
|
|
12
|
+
export declare function getSceneRegions(client: PostgresClient, projectId: string, sceneId: string): Promise<SceneRegionRecord[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Project-wide region names (no boxes) — the whole spatial vocabulary in one
|
|
15
|
+
* read, for a scene/region picker or an agent's project context.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listSceneRegions(client: PostgresClient, projectId: string): Promise<SceneRegionSummary[]>;
|
|
18
|
+
//# sourceMappingURL=sceneRegions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sceneRegions.d.ts","sourceRoot":"","sources":["../src/sceneRegions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAoB,MAAM,aAAa,CAAC;AAEpE,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,CAAC;AAiDtD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,WAAW,EAAE,GAC9B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAuB9B;AAED,iFAAiF;AACjF,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAE9B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAY/B"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const SELECT_COLS = `project_id, scene_id, region_id, label, description, bounds,
|
|
2
|
+
(EXTRACT(EPOCH FROM updated_at) * 1000)::bigint AS updated_at_ms`;
|
|
3
|
+
function rowToRegion(row) {
|
|
4
|
+
return {
|
|
5
|
+
projectId: row.project_id,
|
|
6
|
+
sceneId: row.scene_id,
|
|
7
|
+
regionId: row.region_id,
|
|
8
|
+
label: row.label,
|
|
9
|
+
description: row.description,
|
|
10
|
+
bounds: JSON.parse(row.bounds),
|
|
11
|
+
updatedAt: new Date(row.updated_at_ms),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function selectRegions(executor, projectId, sceneId) {
|
|
15
|
+
const rows = await executor.query(`SELECT ${SELECT_COLS} FROM scene_regions
|
|
16
|
+
WHERE project_id = $1 AND scene_id = $2
|
|
17
|
+
ORDER BY region_id`, [projectId, sceneId]);
|
|
18
|
+
return rows.map(rowToRegion);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Replace the whole region set of `(projectId, sceneId)` with `regions` and
|
|
22
|
+
* return the stored rows. The delete + inserts run in one transaction, so a
|
|
23
|
+
* concurrent reader never sees a half-replaced set.
|
|
24
|
+
*/
|
|
25
|
+
export async function putSceneRegions(client, projectId, sceneId, regions) {
|
|
26
|
+
return client.transaction(async (tx) => {
|
|
27
|
+
await tx.query(`DELETE FROM scene_regions WHERE project_id = $1 AND scene_id = $2`, [
|
|
28
|
+
projectId,
|
|
29
|
+
sceneId,
|
|
30
|
+
]);
|
|
31
|
+
for (const region of regions) {
|
|
32
|
+
await tx.query(`INSERT INTO scene_regions
|
|
33
|
+
(project_id, scene_id, region_id, label, description, bounds, updated_at)
|
|
34
|
+
VALUES ($1, $2, $3, $4, $5, $6, (now() AT TIME ZONE 'utc'))`, [
|
|
35
|
+
projectId,
|
|
36
|
+
sceneId,
|
|
37
|
+
region.id,
|
|
38
|
+
region.label,
|
|
39
|
+
region.description ?? null,
|
|
40
|
+
JSON.stringify(region.bounds),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
return selectRegions(tx, projectId, sceneId);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Read one scene's regions, ordered by region id (stable for callers/diffs). */
|
|
47
|
+
export async function getSceneRegions(client, projectId, sceneId) {
|
|
48
|
+
return selectRegions(client, projectId, sceneId);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Project-wide region names (no boxes) — the whole spatial vocabulary in one
|
|
52
|
+
* read, for a scene/region picker or an agent's project context.
|
|
53
|
+
*/
|
|
54
|
+
export async function listSceneRegions(client, projectId) {
|
|
55
|
+
const rows = await client.query(`SELECT scene_id, region_id, label FROM scene_regions
|
|
56
|
+
WHERE project_id = $1
|
|
57
|
+
ORDER BY scene_id, region_id`, [projectId]);
|
|
58
|
+
return rows.map((row) => ({
|
|
59
|
+
sceneId: row.scene_id,
|
|
60
|
+
regionId: row.region_id,
|
|
61
|
+
label: row.label,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=sceneRegions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sceneRegions.js","sourceRoot":"","sources":["../src/sceneRegions.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,GAAG;wEACoD,CAAC;AAEzE,SAAS,WAAW,CAAC,GAAc;IACjC,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,OAAO,EAAE,GAAG,CAAC,QAAQ;QACrB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAS;QACtC,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,QAA0B,EAC1B,SAAiB,EACjB,OAAe;IAEf,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,KAAK,CAC/B,UAAU,WAAW;;wBAED,EACpB,CAAC,SAAS,EAAE,OAAO,CAAC,CACrB,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,SAAiB,EACjB,OAAe,EACf,OAA+B;IAE/B,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,mEAAmE,EAAE;YAClF,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,EAAE,CAAC,KAAK,CACZ;;qEAE6D,EAC7D;gBACE,SAAS;gBACT,OAAO;gBACP,MAAM,CAAC,EAAE;gBACT,MAAM,CAAC,KAAK;gBACZ,MAAM,CAAC,WAAW,IAAI,IAAI;gBAC1B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC;aAC9B,CACF,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,SAAiB,EACjB,OAAe;IAEf,OAAO,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAsB,EACtB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAC7B;;kCAE8B,EAC9B,CAAC,SAAS,CAAC,CACZ,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACxB,OAAO,EAAE,GAAG,CAAC,QAAQ;QACrB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,KAAK,EAAE,GAAG,CAAC,KAAK;KACjB,CAAC,CAAC,CAAC;AACN,CAAC"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @uptimizr/db-postgres
|
|
2
|
+
|
|
3
|
+
> The optional single-tenant PostgreSQL store for the Uptimizr collector (ADR 0020) — a familiar,
|
|
4
|
+
> multi-writer relational backend for teams that already run Postgres. A re-home + dialect
|
|
5
|
+
> emitter, not a rewrite: every aggregation is authored once in `@uptimizr/db` and rendered with
|
|
6
|
+
> the shared `postgresDialect`. Server/Node only; select it with `COLLECTOR_STORE=postgres`.
|
|
7
|
+
|
|
8
|
+
## Docs
|
|
9
|
+
|
|
10
|
+
- [Package reference](./README.md): when to use it, env vars, the row-store fit gaps and how they are closed, file layout.
|
|
11
|
+
- [Agent guide](./AGENTS.md): rules, canonical usage, parity and the `POSTGRES_PARITY_REQUIRED` gate.
|
|
12
|
+
- [Storage contracts + query layer](../db/README.md): `@uptimizr/db`, the `buildX`/`Dialect` layer, the parity harness.
|
|
13
|
+
- [Collector configuration](https://github.com/RaananW/Uptimizr/blob/main/docs/integration.md): `COLLECTOR_STORE` and the HTTP API.
|
|
14
|
+
- [Architecture Decision Records](https://github.com/RaananW/Uptimizr/tree/main/docs/adr): database choice (0002), privacy model (0003), migrations (0007), open-core storage boundary (0020), AI-first analytics layer (0051).
|
|
15
|
+
|
|
16
|
+
## Environment
|
|
17
|
+
|
|
18
|
+
- `COLLECTOR_STORE=postgres` selects this store.
|
|
19
|
+
- `POSTGRES_URL` (or `DATABASE_URL`) — libpq URI; the database must already exist. Postgres 14+.
|
|
20
|
+
- `POSTGRES_SCHEMA` (default `public`, created on first boot), `POSTGRES_POOL_MAX` (default `10`).
|
|
21
|
+
- `POSTGRES_PARITY_REQUIRED=1` makes the live parity suites fail instead of skipping (the CI job sets it).
|
|
22
|
+
|
|
23
|
+
## Key exports
|
|
24
|
+
|
|
25
|
+
- Client: `createPostgresClient(settings)`, `assertSafeIdentifier`, types `PostgresClient` / `PostgresExecutor` / `PostgresRow`.
|
|
26
|
+
- Migrations: `POSTGRES_MIGRATIONS`, `migratePostgres(client)` — forward-only, idempotent, advisory-locked (ADR 0007).
|
|
27
|
+
- Queries: `runPostgresQuery(client, spec)` — rewrites named to positional params and coerces numeric columns at the driver edge (ADR 0051 §2).
|
|
28
|
+
- Events: `insertEvents`, `getSessionEvents`, `streamSessionEvents`, `getSessionMeta`.
|
|
29
|
+
- Metadata: `createProject`, `getProject`, `createApiKey`, `resolveApiKey`, `hashApiKey`, `apiKeyPrefix`, `generateApiKey` (keys stored only as SHA-256 hashes).
|
|
30
|
+
- Audit: `recordAudit`, `listAudit`, `pruneAudit` (key ids, never keys).
|
|
31
|
+
- Scene registry: `upsertSceneProxy`, `getSceneRepresentation`, `listSceneRepresentations`, `putSceneRegions`, `getSceneRegions`, `listSceneRegions`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uptimizr/db-postgres",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Optional single-tenant PostgreSQL store for Uptimizr — composes the @uptimizr/db dialect-agnostic query layer and CollectorStore contract for self-hosters who already run Postgres and want a multi-writer relational backend.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"uptimizr",
|
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"README.md",
|
|
39
|
-
"LICENSE"
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"AGENTS.md",
|
|
41
|
+
"llms.txt"
|
|
40
42
|
],
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"pg": "^8.23.0",
|
|
43
|
-
"@uptimizr/db": "
|
|
44
|
-
"@uptimizr/schema": "1.0
|
|
45
|
+
"@uptimizr/db": "2.0.0",
|
|
46
|
+
"@uptimizr/schema": "1.1.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/node": "^26.4.1",
|