@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.
Files changed (89) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/dist/audit.d.ts +41 -0
  4. package/dist/audit.d.ts.map +1 -0
  5. package/dist/audit.js +18 -0
  6. package/dist/audit.js.map +1 -0
  7. package/dist/authz/cache.d.ts +20 -0
  8. package/dist/authz/cache.d.ts.map +1 -1
  9. package/dist/authz/cache.js +29 -0
  10. package/dist/authz/cache.js.map +1 -1
  11. package/dist/authz/filter.d.ts +54 -3
  12. package/dist/authz/filter.d.ts.map +1 -1
  13. package/dist/authz/filter.js +45 -9
  14. package/dist/authz/filter.js.map +1 -1
  15. package/dist/authz/index.d.ts +3 -3
  16. package/dist/authz/index.d.ts.map +1 -1
  17. package/dist/authz/index.js +2 -2
  18. package/dist/authz/index.js.map +1 -1
  19. package/dist/config.d.ts +55 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +245 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/db/client.d.ts +39 -0
  24. package/dist/db/client.d.ts.map +1 -1
  25. package/dist/db/client.js +82 -0
  26. package/dist/db/client.js.map +1 -1
  27. package/dist/db/migrate.d.ts.map +1 -1
  28. package/dist/db/migrate.js +49 -0
  29. package/dist/db/migrate.js.map +1 -1
  30. package/dist/index.d.ts +21 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/lifecycle.d.ts +57 -0
  35. package/dist/lifecycle.d.ts.map +1 -0
  36. package/dist/lifecycle.js +84 -0
  37. package/dist/lifecycle.js.map +1 -0
  38. package/dist/logging.d.ts +46 -0
  39. package/dist/logging.d.ts.map +1 -0
  40. package/dist/logging.js +110 -0
  41. package/dist/logging.js.map +1 -0
  42. package/dist/metadata.d.ts +50 -0
  43. package/dist/metadata.d.ts.map +1 -0
  44. package/dist/metadata.js +127 -0
  45. package/dist/metadata.js.map +1 -0
  46. package/dist/metrics.d.ts +19 -1
  47. package/dist/metrics.d.ts.map +1 -1
  48. package/dist/metrics.js +84 -2
  49. package/dist/metrics.js.map +1 -1
  50. package/dist/migrations/0008_authenticating_lookup.sql +124 -0
  51. package/dist/migrations/0009_refresh_tokens.sql +73 -0
  52. package/dist/migrations/0010_definer_search_path.sql +22 -0
  53. package/dist/migrations/0011_sweep_lease.sql +36 -0
  54. package/dist/migrations/0012_prune.sql +118 -0
  55. package/dist/migrations/0013_reindex.sql +65 -0
  56. package/dist/migrations/0014_retired_collections.sql +68 -0
  57. package/dist/oauth.d.ts +45 -0
  58. package/dist/oauth.d.ts.map +1 -0
  59. package/dist/oauth.js +53 -0
  60. package/dist/oauth.js.map +1 -0
  61. package/dist/observability.d.ts.map +1 -1
  62. package/dist/observability.js +100 -13
  63. package/dist/observability.js.map +1 -1
  64. package/dist/passwords.d.ts +37 -0
  65. package/dist/passwords.d.ts.map +1 -0
  66. package/dist/passwords.js +234 -0
  67. package/dist/passwords.js.map +1 -0
  68. package/dist/redis.d.ts +46 -0
  69. package/dist/redis.d.ts.map +1 -0
  70. package/dist/redis.js +234 -0
  71. package/dist/redis.js.map +1 -0
  72. package/dist/reindex.d.ts +87 -0
  73. package/dist/reindex.d.ts.map +1 -0
  74. package/dist/reindex.js +97 -0
  75. package/dist/reindex.js.map +1 -0
  76. package/dist/s3.d.ts +111 -0
  77. package/dist/s3.d.ts.map +1 -0
  78. package/dist/s3.js +242 -0
  79. package/dist/s3.js.map +1 -0
  80. package/dist/tsconfig.tsbuildinfo +1 -1
  81. package/dist/vector/query.d.ts +49 -4
  82. package/dist/vector/query.d.ts.map +1 -1
  83. package/dist/vector/query.js +43 -10
  84. package/dist/vector/query.js.map +1 -1
  85. package/dist/vector/search.d.ts +175 -4
  86. package/dist/vector/search.d.ts.map +1 -1
  87. package/dist/vector/search.js +320 -7
  88. package/dist/vector/search.js.map +1 -1
  89. 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;
@@ -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;AAG9B,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,CAoFtG"}
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"}
@@ -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
- const counts = await withOrg(pool, org.id, async (c) => (await c.query(`SELECT status, count(*)::text AS n FROM documents
23
- WHERE org_id = $1 AND deleted_at IS NULL GROUP BY status`, [org.id])).rows, scope);
24
- for (const row of counts) {
25
- metrics.documents.set(Number(row.n), { org: org.slug, status: row.status });
26
- }
27
- const pending = await withOrg(pool, org.id, async (c) => (await c.query(`SELECT count(*)::text AS n FROM documents
28
- WHERE org_id = $1 AND deleted_at IS NOT NULL AND vectors_purged_at IS NULL`, [org.id])).rows[0]?.n ?? '0', scope);
29
- metrics.tombstonesPending.set(Number(pending), { org: org.slug });
30
- // The oldest document whose tags predate the current groups_version.
31
- // Zero when everything is caught up; the age of the laggard otherwise.
32
- const lag = await withOrg(pool, org.id, async (c) => (await c.query(`SELECT COALESCE(EXTRACT(EPOCH FROM (now() - min(COALESCE(acl_tagged_at, created_at)))), 0)::text AS lag
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 AND deleted_at IS NULL AND acl_version < $2`, [org.id, Number(org.groups_version)])).rows[0]?.lag ?? '0', scope);
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
- metrics.aclPropagationLag.set(Number(lag), { org: org.slug });
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":"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"}
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"}