@nacre.work/core 0.0.0 → 0.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/dist/audit.d.ts +41 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +18 -0
- package/dist/audit.js.map +1 -0
- package/dist/authz/cache.d.ts +20 -0
- package/dist/authz/cache.d.ts.map +1 -1
- package/dist/authz/cache.js +29 -0
- package/dist/authz/cache.js.map +1 -1
- package/dist/authz/filter.d.ts +54 -3
- package/dist/authz/filter.d.ts.map +1 -1
- package/dist/authz/filter.js +45 -9
- package/dist/authz/filter.js.map +1 -1
- package/dist/authz/index.d.ts +3 -3
- package/dist/authz/index.d.ts.map +1 -1
- package/dist/authz/index.js +2 -2
- package/dist/authz/index.js.map +1 -1
- package/dist/config.d.ts +55 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +245 -7
- package/dist/config.js.map +1 -1
- package/dist/db/client.d.ts +39 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +82 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +49 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +57 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +84 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +110 -0
- package/dist/logging.js.map +1 -0
- package/dist/metadata.d.ts +50 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +127 -0
- package/dist/metadata.js.map +1 -0
- package/dist/metrics.d.ts +19 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +84 -2
- package/dist/metrics.js.map +1 -1
- package/dist/migrations/0008_authenticating_lookup.sql +124 -0
- package/dist/migrations/0009_refresh_tokens.sql +73 -0
- package/dist/migrations/0010_definer_search_path.sql +22 -0
- package/dist/migrations/0011_sweep_lease.sql +36 -0
- package/dist/migrations/0012_prune.sql +118 -0
- package/dist/migrations/0013_reindex.sql +65 -0
- package/dist/migrations/0014_retired_collections.sql +68 -0
- package/dist/oauth.d.ts +45 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +53 -0
- package/dist/oauth.js.map +1 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +100 -13
- package/dist/observability.js.map +1 -1
- package/dist/passwords.d.ts +37 -0
- package/dist/passwords.d.ts.map +1 -0
- package/dist/passwords.js +234 -0
- package/dist/passwords.js.map +1 -0
- package/dist/redis.d.ts +46 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +234 -0
- package/dist/redis.js.map +1 -0
- package/dist/reindex.d.ts +87 -0
- package/dist/reindex.d.ts.map +1 -0
- package/dist/reindex.js +97 -0
- package/dist/reindex.js.map +1 -0
- package/dist/s3.d.ts +111 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +242 -0
- package/dist/s3.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vector/query.d.ts +49 -4
- package/dist/vector/query.d.ts.map +1 -1
- package/dist/vector/query.js +43 -10
- package/dist/vector/query.js.map +1 -1
- package/dist/vector/search.d.ts +175 -4
- package/dist/vector/search.d.ts.map +1 -1
- package/dist/vector/search.js +320 -7
- package/dist/vector/search.js.map +1 -1
- package/package.json +26 -8
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- Two things that grew forever, and the decision about the second one.
|
|
2
|
+
--
|
|
3
|
+
-- ─── refresh tokens ───
|
|
4
|
+
--
|
|
5
|
+
-- `0009` said, in a comment on the index built for it: "Expired rows are
|
|
6
|
+
-- deleted by a sweep rather than kept forever; ordering by expiry is what makes
|
|
7
|
+
-- that sweep cheap." There was no sweep. Every login and every rotation inserts
|
|
8
|
+
-- a row and nothing has ever removed one, so the table grows at the rate people
|
|
9
|
+
-- sign in — and grows fastest on the deployments that rotate most often, which
|
|
10
|
+
-- is to say the ones following the advice.
|
|
11
|
+
--
|
|
12
|
+
-- Nothing schema-side is needed for that: `nacre_worker` already holds DELETE
|
|
13
|
+
-- on `refresh_tokens` from `0009`. The sweep is in the worker. It is mentioned
|
|
14
|
+
-- here because this file is where somebody will come looking for it.
|
|
15
|
+
--
|
|
16
|
+
-- ─── the audit log, which is the actual decision ───
|
|
17
|
+
--
|
|
18
|
+
-- `NACRE_AUDIT_RETENTION_DAYS` has been validated at startup and read by
|
|
19
|
+
-- nothing since it was written, and `docs/audit.md` recorded why: `0002`
|
|
20
|
+
-- revokes DELETE on `audit_events` from `nacre_app` on purpose, so retention
|
|
21
|
+
-- "has to run as the owner. That is a decision still to be made rather than a
|
|
22
|
+
-- line still to be written."
|
|
23
|
+
--
|
|
24
|
+
-- This is the decision. Retention is implemented, and the append-only guarantee
|
|
25
|
+
-- is not weakened, because the two are not in tension once you say what
|
|
26
|
+
-- append-only is protecting: it stops history being *rewritten* — a specific
|
|
27
|
+
-- inconvenient event erased, a result flipped from deny to allow. Expiring
|
|
28
|
+
-- everything older than a published horizon is the opposite of that. It is
|
|
29
|
+
-- indiscriminate by construction, it is announced in the configuration, and an
|
|
30
|
+
-- auditor can compute exactly which window survives.
|
|
31
|
+
--
|
|
32
|
+
-- So the grant stays revoked and the pruning goes through one SECURITY DEFINER
|
|
33
|
+
-- function whose *shape* is the guarantee, because its shape is the only part
|
|
34
|
+
-- that holds against every caller:
|
|
35
|
+
--
|
|
36
|
+
-- * it takes a retention in days, never a predicate, so no caller can name a
|
|
37
|
+
-- row, an actor, an organization, or a result;
|
|
38
|
+
-- * it refuses a retention below 30 days, so it cannot be turned into "delete
|
|
39
|
+
-- everything" or "delete this morning";
|
|
40
|
+
-- * it deletes at most `max_rows` per call, so retention on a long-neglected
|
|
41
|
+
-- table is many small transactions rather than one that holds locks for an
|
|
42
|
+
-- hour;
|
|
43
|
+
-- * it returns the count, so the worker can log what it did and a run that
|
|
44
|
+
-- deleted a surprising number is visible rather than silent.
|
|
45
|
+
--
|
|
46
|
+
-- ─── what the EXECUTE grant below does and does not buy ───
|
|
47
|
+
--
|
|
48
|
+
-- It states intent and it stops third parties; it does **not** fence the
|
|
49
|
+
-- application off, and claiming otherwise here would be worse than not saying
|
|
50
|
+
-- it. `0008` grants `nacre_worker` to `nacre_app`, and membership inherits by
|
|
51
|
+
-- default, so the API's role can call this function without so much as a
|
|
52
|
+
-- SET ROLE. Verified rather than assumed — `SET ROLE nacre_app;
|
|
53
|
+
-- SELECT prune_audit_events(400, 10)` succeeds.
|
|
54
|
+
--
|
|
55
|
+
-- That is fine, and it is fine for a reason worth writing down: the four
|
|
56
|
+
-- properties above do not depend on who is calling. `nacre_app` reaching this
|
|
57
|
+
-- function can expire a window past a published horizon, which the operator
|
|
58
|
+
-- configured and an auditor can compute — and it still cannot erase a chosen
|
|
59
|
+
-- event, which is the whole content of the append-only guarantee. Its direct
|
|
60
|
+
-- DELETE stays revoked, so the arbitrary path remains closed.
|
|
61
|
+
--
|
|
62
|
+
-- Anyone with the owner's credentials can still do anything at all. That was
|
|
63
|
+
-- already true and no function changes it.
|
|
64
|
+
|
|
65
|
+
-- 30 days. Below this, retention stops being retention and becomes a way to
|
|
66
|
+
-- make recent events go away, which is the thing append-only exists to prevent.
|
|
67
|
+
-- Deliberately not configurable: a floor an operator can lower is not a floor.
|
|
68
|
+
CREATE FUNCTION prune_audit_events(retention_days integer, max_rows integer)
|
|
69
|
+
RETURNS integer
|
|
70
|
+
LANGUAGE plpgsql
|
|
71
|
+
SECURITY DEFINER
|
|
72
|
+
-- Pinned, for the reason 0010 exists: an unqualified name in a definer function
|
|
73
|
+
-- resolves through the caller's search_path, and any role may create objects in
|
|
74
|
+
-- pg_temp. A shadowed `audit_events` here would be a caller choosing which rows
|
|
75
|
+
-- a privileged function deletes.
|
|
76
|
+
SET search_path = pg_catalog, public
|
|
77
|
+
AS $$
|
|
78
|
+
DECLARE
|
|
79
|
+
removed integer;
|
|
80
|
+
BEGIN
|
|
81
|
+
IF retention_days < 30 THEN
|
|
82
|
+
RAISE EXCEPTION 'retention below the 30 day floor: %', retention_days
|
|
83
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
84
|
+
END IF;
|
|
85
|
+
IF max_rows < 1 OR max_rows > 100000 THEN
|
|
86
|
+
RAISE EXCEPTION 'max_rows out of range: %', max_rows
|
|
87
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
88
|
+
END IF;
|
|
89
|
+
|
|
90
|
+
-- By ctid over a bounded, ordered subquery: the index is
|
|
91
|
+
-- (org_id, occurred_at DESC), so an unbounded `DELETE ... WHERE
|
|
92
|
+
-- occurred_at < cutoff` on a table nobody has pruned in a year is one
|
|
93
|
+
-- statement scanning the lot and holding every row lock it takes until it
|
|
94
|
+
-- commits. Oldest first so repeated calls converge.
|
|
95
|
+
WITH doomed AS (
|
|
96
|
+
SELECT ctid
|
|
97
|
+
FROM audit_events
|
|
98
|
+
WHERE occurred_at < now() - make_interval(days => retention_days)
|
|
99
|
+
ORDER BY occurred_at
|
|
100
|
+
LIMIT max_rows
|
|
101
|
+
)
|
|
102
|
+
DELETE FROM audit_events a USING doomed d WHERE a.ctid = d.ctid;
|
|
103
|
+
|
|
104
|
+
GET DIAGNOSTICS removed = ROW_COUNT;
|
|
105
|
+
RETURN removed;
|
|
106
|
+
END
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
-- The worker, and only the worker. `nacre_app` is the role on the process that
|
|
110
|
+
-- serves requests; it has no business pruning anything, and 0002 revoked its
|
|
111
|
+
-- DELETE for exactly that reason.
|
|
112
|
+
REVOKE ALL ON FUNCTION prune_audit_events(integer, integer) FROM PUBLIC;
|
|
113
|
+
GRANT EXECUTE ON FUNCTION prune_audit_events(integer, integer) TO nacre_worker;
|
|
114
|
+
|
|
115
|
+
-- Expiry alone is not enough to find the sweep's rows cheaply once a family has
|
|
116
|
+
-- been revoked for a month and a half: 0009's index is on `expires_at`, which
|
|
117
|
+
-- is what the sweep orders by, and that one already exists. Nothing further is
|
|
118
|
+
-- needed here — noted so the next person does not add a redundant index.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Reindexing a layer onto a different embedding model.
|
|
2
|
+
--
|
|
3
|
+
-- `layers.reindex_state` has been in the schema since 0001 with a comment
|
|
4
|
+
-- describing its shape, and nothing has ever written it. This is what writes it,
|
|
5
|
+
-- plus the one column the pass needs to know what is left to do.
|
|
6
|
+
--
|
|
7
|
+
-- ─── what a reindex is, mechanically ───
|
|
8
|
+
--
|
|
9
|
+
-- Chunks already hold their text and their Qdrant point id. So reindexing is
|
|
10
|
+
-- not re-parsing anything: it is reading stored chunk text, embedding it with
|
|
11
|
+
-- the new model, and adding a second **named vector** to the points that are
|
|
12
|
+
-- already there. The old vector stays where it is and search keeps using it,
|
|
13
|
+
-- which is what "search stays available throughout" in docs/architecture.md
|
|
14
|
+
-- means in practice.
|
|
15
|
+
--
|
|
16
|
+
-- When every live document in the layer carries the new vector, `vector_name`
|
|
17
|
+
-- switches in one statement and the next query uses it.
|
|
18
|
+
--
|
|
19
|
+
-- ─── one column, and why not dual-write ───
|
|
20
|
+
--
|
|
21
|
+
-- `reindexed_vector` records which named vector a document has most recently
|
|
22
|
+
-- been reindexed onto. The pass selects documents in a reindexing layer where
|
|
23
|
+
-- it is distinct from the shadow vector; the switch happens when that set is
|
|
24
|
+
-- empty.
|
|
25
|
+
--
|
|
26
|
+
-- docs/architecture.md step 2 says "put the layer in dual-write: new documents
|
|
27
|
+
-- are indexed by both models". This does not do that, and the deviation is
|
|
28
|
+
-- deliberate. Dual-write means the ingest path carries reindex awareness
|
|
29
|
+
-- permanently — a branch on layer state in the hot path, for a condition that
|
|
30
|
+
-- is true for a few hours in a layer's life. Selecting on "lacks the shadow
|
|
31
|
+
-- vector" gets the same guarantee out of machinery that already exists: a
|
|
32
|
+
-- document ingested mid-reindex simply has no shadow vector yet, so the same
|
|
33
|
+
-- pass picks it up, and the switch cannot happen while it is outstanding.
|
|
34
|
+
--
|
|
35
|
+
-- The resulting invariant is stronger and easier to check than dual-write's:
|
|
36
|
+
-- **`vector_name` only ever changes when no live document in the layer lacks
|
|
37
|
+
-- the new vector.** One predicate, verifiable in SQL, rather than a promise
|
|
38
|
+
-- about two write paths staying in step.
|
|
39
|
+
|
|
40
|
+
ALTER TABLE documents ADD COLUMN reindexed_vector text;
|
|
41
|
+
|
|
42
|
+
-- Partial and on the layer, because the pass always asks the same question:
|
|
43
|
+
-- "in this layer, which documents do not yet carry this vector". Live documents
|
|
44
|
+
-- with chunks are the only ones that can — the rest have no points to add a
|
|
45
|
+
-- vector to.
|
|
46
|
+
CREATE INDEX documents_reindex_pending ON documents (layer_id, reindexed_vector)
|
|
47
|
+
WHERE deleted_at IS NULL AND chunk_count > 0;
|
|
48
|
+
|
|
49
|
+
-- ─── the state, and the one shape it is allowed to take ───
|
|
50
|
+
--
|
|
51
|
+
-- A check constraint rather than a convention. `reindex_state` is jsonb, which
|
|
52
|
+
-- means anything at all fits in the column, and the failure mode of a free-form
|
|
53
|
+
-- status field is a typo that reads as "not running" — leaving a reindex that
|
|
54
|
+
-- has stopped looking exactly like one that never started.
|
|
55
|
+
--
|
|
56
|
+
-- NULL means no reindex has ever run, which is different from one that finished
|
|
57
|
+
-- and is why the column is nullable rather than defaulted.
|
|
58
|
+
ALTER TABLE layers ADD CONSTRAINT layers_reindex_state_shape CHECK (
|
|
59
|
+
reindex_state IS NULL
|
|
60
|
+
OR (
|
|
61
|
+
reindex_state ? 'status'
|
|
62
|
+
AND reindex_state ->> 'status' IN ('running', 'complete', 'failed')
|
|
63
|
+
AND reindex_state ? 'shadow_vector'
|
|
64
|
+
)
|
|
65
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- The collections a reindex leaves behind.
|
|
2
|
+
--
|
|
3
|
+
-- Every model migration builds a new collection, copies every point across, and
|
|
4
|
+
-- moves `organizations.vector_collection` at the end. The old collection is
|
|
5
|
+
-- still there afterwards, holding a full copy of the organization's vectors,
|
|
6
|
+
-- and nothing has ever removed one. `runbooks/rollback-layer-reindex.md` says
|
|
7
|
+
-- so in as many words — "каждая переиндексация оставляет коллекцию … ничего их
|
|
8
|
+
-- не подчищает" — and hands the operator a shell loop.
|
|
9
|
+
--
|
|
10
|
+
-- That loop is also the wrong rule. It reads every collection Qdrant has,
|
|
11
|
+
-- subtracts the ones an organization points at, and calls the difference
|
|
12
|
+
-- orphaned. During a copy the *target* is exactly that: built, filling up, and
|
|
13
|
+
-- not yet pointed at by anything. Running the documented cleanup while a
|
|
14
|
+
-- migration is copying deletes the migration.
|
|
15
|
+
--
|
|
16
|
+
-- ─── so the record is kept, not derived ───
|
|
17
|
+
--
|
|
18
|
+
-- A row is written when the pointer moves, naming the collection it moved away
|
|
19
|
+
-- from. Nothing else is ever a candidate for deletion. A copy in progress has
|
|
20
|
+
-- no row, because the row is written by the same statement that supersedes it,
|
|
21
|
+
-- so the dangerous case is not merely avoided — it is unrepresentable.
|
|
22
|
+
--
|
|
23
|
+
-- The delay is the point of the window, not an implementation detail: D2 in the
|
|
24
|
+
-- rollback runbook is "move the pointer back", which is instant and only works
|
|
25
|
+
-- while the old collection still exists. `NACRE_COLLECTION_RETENTION_DAYS` is
|
|
26
|
+
-- how long that stays possible.
|
|
27
|
+
--
|
|
28
|
+
-- ─── what this does not cover ───
|
|
29
|
+
--
|
|
30
|
+
-- A copy that was abandoned — the reindex failed partway — leaves a target
|
|
31
|
+
-- collection with no row and no pointer, and the sweep will not touch it. That
|
|
32
|
+
-- is deliberate for now: the safe rule is "delete only what a successful
|
|
33
|
+
-- migration superseded", and widening it to "delete anything unreferenced"
|
|
34
|
+
-- brings back the race above. The runbook keeps its manual procedure for that
|
|
35
|
+
-- case alone.
|
|
36
|
+
|
|
37
|
+
CREATE TABLE retired_collections (
|
|
38
|
+
org_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE,
|
|
39
|
+
-- The Qdrant collection name, which is the whole payload of a row. Not a
|
|
40
|
+
-- reference to anything: by the time this matters the collection may
|
|
41
|
+
-- already be gone, and a dangling name is the normal end state.
|
|
42
|
+
name text NOT NULL,
|
|
43
|
+
retired_at timestamptz NOT NULL DEFAULT now(),
|
|
44
|
+
|
|
45
|
+
-- One row per name per organization. A second migration away from the same
|
|
46
|
+
-- collection cannot happen — the pointer only ever leaves a name once — but
|
|
47
|
+
-- an idempotent insert is cheaper to reason about than a proof that it
|
|
48
|
+
-- cannot repeat.
|
|
49
|
+
PRIMARY KEY (org_id, name)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- The sweep asks one question: which rows are older than the window. It asks it
|
|
53
|
+
-- across organizations, so the index is on the timestamp alone.
|
|
54
|
+
CREATE INDEX retired_collections_due ON retired_collections (retired_at);
|
|
55
|
+
|
|
56
|
+
-- Tenant data, so RLS, and FORCE because ENABLE alone does not apply to the
|
|
57
|
+
-- owner and migrations run as the owner.
|
|
58
|
+
ALTER TABLE retired_collections ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
ALTER TABLE retired_collections FORCE ROW LEVEL SECURITY;
|
|
60
|
+
|
|
61
|
+
CREATE POLICY org_isolation ON retired_collections
|
|
62
|
+
USING (org_id = current_setting('app.current_org')::uuid);
|
|
63
|
+
|
|
64
|
+
-- `nacre_app` inserts, because the pointer moves under `withOrg` in the worker's
|
|
65
|
+
-- copy phase and that runs as the application role. `nacre_worker` deletes,
|
|
66
|
+
-- because the sweep runs across organizations.
|
|
67
|
+
GRANT SELECT, INSERT ON retired_collections TO nacre_app;
|
|
68
|
+
GRANT SELECT, INSERT, DELETE ON retired_collections TO nacre_worker;
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protected resource metadata — RFC 9728.
|
|
3
|
+
*
|
|
4
|
+
* Every `401` from the MCP transport carries
|
|
5
|
+
* `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"`,
|
|
6
|
+
* and for as long as that header existed nothing served the path it named. A
|
|
7
|
+
* client doing exactly what the header told it to got a `404`, which is the
|
|
8
|
+
* same failure as a search parameter read by nothing: the server described a
|
|
9
|
+
* capability and did not have it.
|
|
10
|
+
*
|
|
11
|
+
* ## This is a resource server, not an authorization server
|
|
12
|
+
*
|
|
13
|
+
* `docs/mcp.md` says so and the code agrees: tokens are verified locally
|
|
14
|
+
* against a key, and nothing here issues one through an OAuth flow.
|
|
15
|
+
* `/v1/auth/login` is email and password, and a service account key is a random
|
|
16
|
+
* string matched against a hash — neither is an OAuth grant.
|
|
17
|
+
*
|
|
18
|
+
* So `authorization_servers` is **absent unless an operator names one**. The
|
|
19
|
+
* field is optional in RFC 9728, and an installation that has not put an
|
|
20
|
+
* identity provider in front of Nacre has no authorization server to name.
|
|
21
|
+
* Pointing the field at ourselves would be the same lie the 404 was, one
|
|
22
|
+
* redirect further along: a client would fetch our metadata, learn we are our
|
|
23
|
+
* own issuer, ask us for a token endpoint, and find nothing again.
|
|
24
|
+
*
|
|
25
|
+
* What the document is still worth without that field is the part clients
|
|
26
|
+
* actually need first: the canonical resource identifier every token must be
|
|
27
|
+
* audience-bound to, and where to read about the credential that does work
|
|
28
|
+
* today.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProtectedResourceMetadata {
|
|
31
|
+
readonly resource: string;
|
|
32
|
+
readonly authorization_servers?: readonly string[];
|
|
33
|
+
readonly bearer_methods_supported: readonly string[];
|
|
34
|
+
readonly resource_documentation: string;
|
|
35
|
+
readonly scopes_supported: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/** The path RFC 9728 fixes, and the one the `WWW-Authenticate` header names. */
|
|
38
|
+
export declare const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
39
|
+
export declare function protectedResourceMetadata(input: {
|
|
40
|
+
/** `NACRE_CANONICAL_URL`. Baked into the issuer of every token ever minted. */
|
|
41
|
+
readonly canonicalUrl: string;
|
|
42
|
+
/** `NACRE_OAUTH_AUTHORIZATION_SERVER`, when a deployment has one. */
|
|
43
|
+
readonly authorizationServer?: string;
|
|
44
|
+
}): ProtectedResourceMetadata;
|
|
45
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClD,QAAQ,CAAC,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAA;IACpD,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAA;IACvC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7C;AAED,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,0CAA0C,CAAA;AAE9E,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAC/C,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC,GAAG,yBAAyB,CAoB5B"}
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protected resource metadata — RFC 9728.
|
|
3
|
+
*
|
|
4
|
+
* Every `401` from the MCP transport carries
|
|
5
|
+
* `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"`,
|
|
6
|
+
* and for as long as that header existed nothing served the path it named. A
|
|
7
|
+
* client doing exactly what the header told it to got a `404`, which is the
|
|
8
|
+
* same failure as a search parameter read by nothing: the server described a
|
|
9
|
+
* capability and did not have it.
|
|
10
|
+
*
|
|
11
|
+
* ## This is a resource server, not an authorization server
|
|
12
|
+
*
|
|
13
|
+
* `docs/mcp.md` says so and the code agrees: tokens are verified locally
|
|
14
|
+
* against a key, and nothing here issues one through an OAuth flow.
|
|
15
|
+
* `/v1/auth/login` is email and password, and a service account key is a random
|
|
16
|
+
* string matched against a hash — neither is an OAuth grant.
|
|
17
|
+
*
|
|
18
|
+
* So `authorization_servers` is **absent unless an operator names one**. The
|
|
19
|
+
* field is optional in RFC 9728, and an installation that has not put an
|
|
20
|
+
* identity provider in front of Nacre has no authorization server to name.
|
|
21
|
+
* Pointing the field at ourselves would be the same lie the 404 was, one
|
|
22
|
+
* redirect further along: a client would fetch our metadata, learn we are our
|
|
23
|
+
* own issuer, ask us for a token endpoint, and find nothing again.
|
|
24
|
+
*
|
|
25
|
+
* What the document is still worth without that field is the part clients
|
|
26
|
+
* actually need first: the canonical resource identifier every token must be
|
|
27
|
+
* audience-bound to, and where to read about the credential that does work
|
|
28
|
+
* today.
|
|
29
|
+
*/
|
|
30
|
+
/** The path RFC 9728 fixes, and the one the `WWW-Authenticate` header names. */
|
|
31
|
+
export const PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource';
|
|
32
|
+
export function protectedResourceMetadata(input) {
|
|
33
|
+
return {
|
|
34
|
+
// Without a trailing slash, because this is the audience value a token is
|
|
35
|
+
// bound to and `https://api.example.com` and `https://api.example.com/` are
|
|
36
|
+
// different strings to every audience check ever written.
|
|
37
|
+
resource: input.canonicalUrl.replace(/\/+$/, ''),
|
|
38
|
+
...(input.authorizationServer === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { authorization_servers: [input.authorizationServer.replace(/\/+$/, '')] }),
|
|
41
|
+
// Header only. A bearer token in a query string ends up in access logs,
|
|
42
|
+
// proxy logs and browser history, and RFC 6750 has deprecated that form for
|
|
43
|
+
// long enough that offering it is a liability rather than a courtesy.
|
|
44
|
+
bearer_methods_supported: ['header'],
|
|
45
|
+
resource_documentation: 'https://github.com/nacre-work/nacre/blob/main/docs/mcp.md',
|
|
46
|
+
// Nacre does not scope tokens. Permission is computed per call against the
|
|
47
|
+
// grant graph, so a token carries an organization and a role and never a
|
|
48
|
+
// list of what it may reach — which is why this is empty rather than
|
|
49
|
+
// inventing names no code reads. See docs/authz.md.
|
|
50
|
+
scopes_supported: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAUH,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,uCAAuC,CAAA;AAE9E,MAAM,UAAU,yBAAyB,CAAC,KAKzC;IACC,OAAO;QACL,0EAA0E;QAC1E,4EAA4E;QAC5E,0DAA0D;QAC1D,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAChD,GAAG,CAAC,KAAK,CAAC,mBAAmB,KAAK,SAAS;YACzC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,qBAAqB,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QAC/E,wEAAwE;QACxE,4EAA4E;QAC5E,sEAAsE;QACtE,wBAAwB,EAAE,CAAC,QAAQ,CAAC;QACpC,sBAAsB,EAAE,2DAA2D;QACnF,2EAA2E;QAC3E,yEAAyE;QACzE,qEAAqE;QACrE,oDAAoD;QACpD,gBAAgB,EAAE,EAAE;KACrB,CAAA;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAA;AAI9B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAkKtG"}
|
package/dist/observability.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withOrg } from './db/client.js';
|
|
2
|
+
import { fromStateJson, reindexProgress } from './reindex.js';
|
|
2
3
|
/**
|
|
3
4
|
* The gauges that are queries rather than counters.
|
|
4
5
|
*
|
|
@@ -18,20 +19,105 @@ export function collectDatabaseGauges(pool, metrics, role) {
|
|
|
18
19
|
metrics.documents.reset();
|
|
19
20
|
metrics.tombstonesPending.reset();
|
|
20
21
|
metrics.aclPropagationLag.reset();
|
|
22
|
+
metrics.reindexProgress.reset();
|
|
23
|
+
metrics.collectionsRetired.reset();
|
|
24
|
+
// One series per layer that has ever been reindexed, labelled by slug
|
|
25
|
+
// like every other per-tenant gauge here. Layers with no reindex produce
|
|
26
|
+
// no row and therefore no series: inventing a zero for them would mean
|
|
27
|
+
// every layer in the installation permanently reading "reindex started,
|
|
28
|
+
// gone nowhere".
|
|
29
|
+
//
|
|
30
|
+
// Across organizations in one query rather than per tenant, because this
|
|
31
|
+
// reads `layers` and not `documents` — a handful of rows per tenant at
|
|
32
|
+
// most, and the whole point of the collapse in this collector was that a
|
|
33
|
+
// scrape must not cost a query per organization.
|
|
34
|
+
const { rows: reindexing } = await client.query(`SELECT o.slug, l.slug AS layer, l.reindex_state AS state
|
|
35
|
+
FROM layers l
|
|
36
|
+
JOIN organizations o ON o.id = l.org_id
|
|
37
|
+
WHERE l.reindex_state IS NOT NULL
|
|
38
|
+
AND l.deleted_at IS NULL AND o.deleted_at IS NULL`);
|
|
39
|
+
for (const row of reindexing) {
|
|
40
|
+
const state = fromStateJson(row.state);
|
|
41
|
+
if (state === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
metrics.reindexProgress.set(reindexProgress(state), { org: row.slug, layer: row.layer });
|
|
44
|
+
}
|
|
45
|
+
// Collections a finished migration is still holding, for the same reason
|
|
46
|
+
// and in the same shape as the reindex gauge above: one query across
|
|
47
|
+
// organizations, because `retired_collections` holds a handful of rows in
|
|
48
|
+
// total rather than a handful per tenant, and a scrape must not cost a
|
|
49
|
+
// query per organization.
|
|
50
|
+
//
|
|
51
|
+
// Only organizations that have one produce a series. A zero for everybody
|
|
52
|
+
// else would be true and useless — the number that matters is how much
|
|
53
|
+
// disk a completed migration has not given back, and the alertable shape
|
|
54
|
+
// is one that stops falling, which needs a series to exist at all rather
|
|
55
|
+
// than a floor under every tenant.
|
|
56
|
+
const { rows: retained } = await client.query(`SELECT o.slug, count(*) AS n
|
|
57
|
+
FROM retired_collections r
|
|
58
|
+
JOIN organizations o ON o.id = r.org_id
|
|
59
|
+
GROUP BY o.slug`);
|
|
60
|
+
for (const row of retained) {
|
|
61
|
+
metrics.collectionsRetired.set(Number(row.n), { org: row.slug });
|
|
62
|
+
}
|
|
21
63
|
for (const org of orgs) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
64
|
+
// One query per organization, not three.
|
|
65
|
+
//
|
|
66
|
+
// It was three — document counts, pending tombstones, propagation lag —
|
|
67
|
+
// each a separate round trip inside its own `withOrg`, so a scrape cost
|
|
68
|
+
// 3N+1 queries and each of those three also paid for a connection
|
|
69
|
+
// checkout and a `SET LOCAL app.current_org`. At five hundred tenants
|
|
70
|
+
// that is fifteen hundred queries every fifteen seconds, on the same
|
|
71
|
+
// pool the request path uses, triggered by an endpoint that anyone who
|
|
72
|
+
// can reach the port may call as fast as they like.
|
|
73
|
+
//
|
|
74
|
+
// All three read `documents` for one organization, so they are one
|
|
75
|
+
// grouped query with FILTERed aggregates. Still per organization and
|
|
76
|
+
// still inside `withOrg`: collapsing across tenants would mean reading
|
|
77
|
+
// every tenant's documents from the API process, and the row-level
|
|
78
|
+
// policies are not something to route around for a gauge.
|
|
79
|
+
const rows = await withOrg(pool, org.id, async (c) => (await c.query(`SELECT status,
|
|
80
|
+
count(*) FILTER (WHERE deleted_at IS NULL)::text AS live,
|
|
81
|
+
count(*) FILTER (
|
|
82
|
+
WHERE deleted_at IS NOT NULL AND vectors_purged_at IS NULL
|
|
83
|
+
)::text AS tombstoned,
|
|
84
|
+
-- The oldest document whose tags predate the current
|
|
85
|
+
-- groups_version. chunk_count > 0 is the whole
|
|
86
|
+
-- correctness of this one, and it was missing once: the
|
|
87
|
+
-- number is the age of the oldest document still
|
|
88
|
+
-- carrying tags built from a superseded grant set, which
|
|
89
|
+
-- only a document with points in the index can do.
|
|
90
|
+
-- Without the clause a single failed or pending
|
|
91
|
+
-- document pinned the gauge at its own age forever,
|
|
92
|
+
-- because the retag loop claims neither and nothing else
|
|
93
|
+
-- can clear it — the one metric with an alert on it
|
|
94
|
+
-- became a stuck alert, which is how it gets muted.
|
|
95
|
+
--
|
|
96
|
+
-- Deliberately not status = 'indexed'. A document that
|
|
97
|
+
-- indexed once and failed on a later pass still has the
|
|
98
|
+
-- earlier pass's points and still carries their tags,
|
|
99
|
+
-- and is exactly the case this is meant to catch.
|
|
100
|
+
-- claimStale uses the same predicate; the two must
|
|
101
|
+
-- agree, or the gauge counts what the loop cannot claim.
|
|
102
|
+
min(COALESCE(acl_tagged_at, created_at)) FILTER (
|
|
103
|
+
WHERE deleted_at IS NULL AND chunk_count > 0
|
|
104
|
+
AND acl_version < $2
|
|
105
|
+
)::text AS oldest_stale
|
|
33
106
|
FROM documents
|
|
34
|
-
WHERE org_id = $1
|
|
107
|
+
WHERE org_id = $1
|
|
108
|
+
GROUP BY status`, [org.id, Number(org.groups_version)])).rows, scope);
|
|
109
|
+
let tombstoned = 0;
|
|
110
|
+
let oldest;
|
|
111
|
+
for (const row of rows) {
|
|
112
|
+
metrics.documents.set(Number(row.live), { org: org.slug, status: row.status });
|
|
113
|
+
tombstoned += Number(row.tombstoned);
|
|
114
|
+
if (row.oldest_stale !== null) {
|
|
115
|
+
const at = Date.parse(row.oldest_stale);
|
|
116
|
+
if (!Number.isNaN(at) && (oldest === undefined || at < oldest))
|
|
117
|
+
oldest = at;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
metrics.tombstonesPending.set(tombstoned, { org: org.slug });
|
|
35
121
|
// Per organization, and set even when it is zero.
|
|
36
122
|
//
|
|
37
123
|
// A single worst-across-tenants number was the first shape of this and
|
|
@@ -44,7 +130,8 @@ export function collectDatabaseGauges(pool, metrics, role) {
|
|
|
44
130
|
// Zero is written rather than omitted: an absent series and a zero one
|
|
45
131
|
// mean "caught up" and "not being measured", and those must not look
|
|
46
132
|
// alike on the one metric that evidences I4.
|
|
47
|
-
|
|
133
|
+
const lag = oldest === undefined ? 0 : Math.max(0, (Date.now() - oldest) / 1000);
|
|
134
|
+
metrics.aclPropagationLag.set(lag, { org: org.slug });
|
|
48
135
|
}
|
|
49
136
|
}
|
|
50
137
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Raised when the process is already verifying as many passwords as it will. */
|
|
2
|
+
export declare class TooBusy extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
/** For a metric and for tests. Not a decision anything makes. */
|
|
6
|
+
export declare const hashingLoad: () => {
|
|
7
|
+
active: number;
|
|
8
|
+
queued: number;
|
|
9
|
+
limit: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* `scrypt$N$r$p$salt$hash`, base64url, parameters first.
|
|
13
|
+
*
|
|
14
|
+
* Self-describing on purpose. A bare hash cannot be verified after the cost is
|
|
15
|
+
* raised, so raising it means invalidating every password in the database;
|
|
16
|
+
* carrying the parameters means old hashes keep verifying and get rewritten as
|
|
17
|
+
* people sign in.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Verify, in time that does not depend on whether the password was right.
|
|
22
|
+
*
|
|
23
|
+
* A malformed or absent hash answers false rather than throwing: the caller
|
|
24
|
+
* turns every failure into the same refusal, and an exception on one of them
|
|
25
|
+
* would be a way to tell them apart from the outside.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a stored hash was made with parameters weaker than the current ones.
|
|
30
|
+
*
|
|
31
|
+
* The caller rehashes on a successful sign-in, which is the only moment the
|
|
32
|
+
* plaintext exists. Without this, raising the cost protects nobody who already
|
|
33
|
+
* has an account.
|
|
34
|
+
*/
|
|
35
|
+
export declare function needsRehash(encoded: string): boolean;
|
|
36
|
+
export declare function spendVerificationTime(password: string): Promise<false>;
|
|
37
|
+
//# sourceMappingURL=passwords.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwords.d.ts","sourceRoot":"","sources":["../passwords.ts"],"names":[],"mappings":"AA8FA,iFAAiF;AACjF,qBAAa,OAAQ,SAAQ,KAAK;;CAKjC;AAyCD,iEAAiE;AACjE,eAAO,MAAM,WAAW,QAAO;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAI5E,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWpE;AAmCD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAexF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIpD;AAgBD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAI5E"}
|