@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,127 @@
1
+ /**
2
+ * Document metadata: what a caller may write, and what they may narrow a search
3
+ * by.
4
+ *
5
+ * One module for both ends on purpose. Ingest and search accept the same shape
6
+ * from the same untrusted place, and two validators would drift — the ingest
7
+ * side would grow a type the search side could not express, or the search side
8
+ * would accept a key ingest had refused, and a caller would be filtering on a
9
+ * field that can never exist.
10
+ *
11
+ * Everything here is a bound. Metadata is written into the vector payload of
12
+ * *every chunk of a document*, so a key is stored once per point and a
13
+ * thousand-chunk document multiplies whatever arrives here by a thousand. The
14
+ * limits are deliberately small; a knowledge index wants `source`, `team`,
15
+ * `updated_at`, not a document's worth of JSON a second time.
16
+ */
17
+ /** Keys per document. */
18
+ export const MAX_METADATA_KEYS = 32;
19
+ export const MAX_METADATA_KEY_LENGTH = 64;
20
+ /** A string value, and every string inside a list value. */
21
+ export const MAX_METADATA_VALUE_LENGTH = 256;
22
+ /** Values in one list. */
23
+ export const MAX_METADATA_LIST = 32;
24
+ /**
25
+ * Keys are `[a-z0-9_]`, lower case, and cannot start with a digit.
26
+ *
27
+ * Narrow because the key becomes a Qdrant payload field name, and Qdrant reads
28
+ * `.` as nested access and `[]` as array indexing — a key containing either
29
+ * would filter on something other than what the caller wrote. Refusing the
30
+ * characters is one line; discovering that `a.b` and `a` `{b}` are the same
31
+ * field is a support ticket.
32
+ */
33
+ const KEY = /^[a-z_][a-z0-9_]*$/;
34
+ export class MetadataError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = 'MetadataError';
38
+ }
39
+ }
40
+ function checkScalar(key, value) {
41
+ if (typeof value === 'string') {
42
+ if (value.length > MAX_METADATA_VALUE_LENGTH) {
43
+ throw new MetadataError(`metadata value for '${key}' is over ${MAX_METADATA_VALUE_LENGTH} characters`);
44
+ }
45
+ return value;
46
+ }
47
+ if (typeof value === 'number') {
48
+ // NaN and the infinities have no JSON representation, so they arrive here
49
+ // only from a caller building the body in code — and they would be written
50
+ // as `null`, which is a value nothing can filter for.
51
+ if (!Number.isFinite(value)) {
52
+ throw new MetadataError(`metadata value for '${key}' is not a finite number`);
53
+ }
54
+ return value;
55
+ }
56
+ if (typeof value === 'boolean')
57
+ return value;
58
+ throw new MetadataError(`metadata value for '${key}' must be a string, number, boolean, or a list of those. ` +
59
+ 'Nested objects are not accepted: the filter surface would have to grow a path ' +
60
+ 'syntax, and a path is a way to reach a field the caller did not name.');
61
+ }
62
+ /**
63
+ * Validate and normalise metadata arriving from a request.
64
+ *
65
+ * Raises rather than dropping what it does not like. A silently discarded key
66
+ * is a document a caller believes is tagged and a filter that will never match
67
+ * it — the same class of failure as a search parameter read by nothing, and the
68
+ * reason `filters` answered 400 rather than being ignored.
69
+ */
70
+ export function parseMetadata(raw, what = 'metadata') {
71
+ if (raw === undefined || raw === null)
72
+ return {};
73
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
74
+ throw new MetadataError(`'${what}' must be an object`);
75
+ }
76
+ const entries = Object.entries(raw);
77
+ if (entries.length > MAX_METADATA_KEYS) {
78
+ throw new MetadataError(`'${what}' has more than ${MAX_METADATA_KEYS} keys`);
79
+ }
80
+ const out = {};
81
+ for (const [key, value] of entries) {
82
+ if (key.length > MAX_METADATA_KEY_LENGTH) {
83
+ throw new MetadataError(`metadata key '${key.slice(0, 20)}…' is over ${MAX_METADATA_KEY_LENGTH} characters`);
84
+ }
85
+ if (!KEY.test(key)) {
86
+ throw new MetadataError(`metadata key '${key}' must match ${String(KEY)} — lower case letters, digits and ` +
87
+ 'underscores, not starting with a digit');
88
+ }
89
+ // `undefined` survives an object literal in TypeScript but not JSON, so it
90
+ // reaches here only from a caller assembling the body in code. Dropping it
91
+ // is the one omission that is not a surprise: the key is absent either way.
92
+ if (value === undefined)
93
+ continue;
94
+ if (Array.isArray(value)) {
95
+ if (value.length > MAX_METADATA_LIST) {
96
+ throw new MetadataError(`metadata value for '${key}' has more than ${MAX_METADATA_LIST} entries`);
97
+ }
98
+ out[key] = value.map((v) => checkScalar(key, v));
99
+ }
100
+ else {
101
+ out[key] = checkScalar(key, value);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * The same bounds, plus the one rule that belongs only to filtering.
108
+ *
109
+ * An empty list is a legitimate *value* — a document may be tagged with no
110
+ * teams — and an impossible *restriction*: "team is one of nothing" matches
111
+ * nothing at all. `buildFilter` refuses to encode it, deliberately, so that
112
+ * what an impossible restriction means stays the caller's decision rather than
113
+ * the query builder's. This is where that decision is made for a request: a 400
114
+ * naming the key, because a client that built a list and put nothing in it has
115
+ * a bug, and answering with an empty result would hide it.
116
+ */
117
+ export function parseFilters(raw) {
118
+ const parsed = parseMetadata(raw, 'filters');
119
+ for (const [key, value] of Object.entries(parsed)) {
120
+ if (Array.isArray(value) && value.length === 0) {
121
+ throw new MetadataError(`filter on '${key}' has no values, so it can match nothing. Drop the key to ` +
122
+ 'search without that restriction.');
123
+ }
124
+ }
125
+ return parsed;
126
+ }
127
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../metadata.ts"],"names":[],"mappings":"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"}
package/dist/metrics.d.ts CHANGED
@@ -52,7 +52,22 @@ export declare class Registry {
52
52
  * counters — document counts, the propagation lag, pending tombstones.
53
53
  */
54
54
  collect(fn: () => Promise<void>): void;
55
- render(): Promise<string>;
55
+ /**
56
+ * How long a collected value is reused.
57
+ *
58
+ * `/metrics` is unauthenticated by default and the collectors are database
59
+ * queries — one per organization. Without this, anyone who can reach the port
60
+ * decides how often the API queries every tenant's `documents` table, on the
61
+ * same pool the request path uses. A scrape loop is a denial of service that
62
+ * looks like monitoring.
63
+ *
64
+ * Ten seconds is shorter than any sensible scrape interval, so a real
65
+ * Prometheus never sees a cached value and the numbers are as live as they
66
+ * were. It only collapses the excess: a second scraper, a human with `watch
67
+ * curl`, or a flood.
68
+ */
69
+ static readonly COLLECT_TTL_MS = 10000;
70
+ render(now?: number): Promise<string>;
56
71
  }
57
72
  /**
58
73
  * The metrics docs/config.md requires.
@@ -67,10 +82,13 @@ export declare function createMetrics(registry: Registry): {
67
82
  searchDuration: Histogram;
68
83
  searchResults: Counter;
69
84
  aclDenials: Counter;
85
+ authFailures: Counter;
70
86
  aclPropagationLag: Gauge;
71
87
  ingestDuration: Histogram;
72
88
  documents: Gauge;
89
+ reindexProgress: Gauge;
73
90
  tombstonesPending: Gauge;
91
+ collectionsRetired: Gauge;
74
92
  };
75
93
  export type Metrics = ReturnType<typeof createMetrics>;
76
94
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAgBrD,UAAU,MAAM;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,CAAA;IAChD,MAAM,IAAI,MAAM,EAAE,CAAA;CACnB;AAED,qBAAa,OAAQ,YAAW,MAAM;;IAKlC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IALvB,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAS;gBAIvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM;IAKvB,GAAG,CAAC,MAAM,GAAE,MAAW,EAAE,EAAE,SAAI,GAAG,IAAI;IAOtC,MAAM,IAAI,MAAM,EAAE;CAOnB;AAED,qBAAa,KAAM,YAAW,MAAM;;IAKhC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IALvB,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;gBAIrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM;IAKvB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI;IAI7C,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,MAAM,EAAE;CAInB;AAID,qBAAa,SAAU,YAAW,MAAM;;IAKpC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE;IANrC,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAS;gBAIzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,SAAS,MAAM,EAAoB;IAKvD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI;IAgBnD,2EAA2E;IACrE,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAS/D,MAAM,IAAI,MAAM,EAAE;CAmBnB;AAED,qBAAa,QAAQ;;IAInB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC;IAQxC;;;OAGG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIhC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;CAahC;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;EA8B/C;AAED,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAA"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAgBrD,UAAU,MAAM;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,CAAA;IAChD,MAAM,IAAI,MAAM,EAAE,CAAA;CACnB;AAED,qBAAa,OAAQ,YAAW,MAAM;;IAKlC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IALvB,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAS;gBAIvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM;IAKvB,GAAG,CAAC,MAAM,GAAE,MAAW,EAAE,EAAE,SAAI,GAAG,IAAI;IAOtC,MAAM,IAAI,MAAM,EAAE;CAOnB;AAED,qBAAa,KAAM,YAAW,MAAM;;IAKhC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IALvB,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;gBAIrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM;IAKvB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI;IAI7C,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,MAAM,EAAE;CAInB;AAID,qBAAa,SAAU,YAAW,MAAM;;IAKpC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE;IANrC,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAS;gBAIzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,SAAS,MAAM,EAAoB;IAKvD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI;IAgBnD,2EAA2E;IACrE,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAS/D,MAAM,IAAI,MAAM,EAAE;CAmBnB;AAED,qBAAa,QAAQ;;IAInB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC;IAQxC;;;OAGG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAItC;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,QAAQ,CAAC,cAAc,SAAS;IAwCjC,MAAM,CAAC,GAAG,GAAE,MAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;CAWxD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;;;;EA4E/C;AAED,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAA"}
package/dist/metrics.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * to keep patched. If the needs here outgrow histograms and gauges, replace it
7
7
  * with prom-client rather than growing this file.
8
8
  */
9
+ var _a;
9
10
  const NAME = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
10
11
  function escapeLabel(value) {
11
12
  return value.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
@@ -141,10 +142,59 @@ export class Registry {
141
142
  collect(fn) {
142
143
  this.#collectors.push(fn);
143
144
  }
144
- async render() {
145
+ /**
146
+ * How long a collected value is reused.
147
+ *
148
+ * `/metrics` is unauthenticated by default and the collectors are database
149
+ * queries — one per organization. Without this, anyone who can reach the port
150
+ * decides how often the API queries every tenant's `documents` table, on the
151
+ * same pool the request path uses. A scrape loop is a denial of service that
152
+ * looks like monitoring.
153
+ *
154
+ * Ten seconds is shorter than any sensible scrape interval, so a real
155
+ * Prometheus never sees a cached value and the numbers are as live as they
156
+ * were. It only collapses the excess: a second scraper, a human with `watch
157
+ * curl`, or a flood.
158
+ */
159
+ static COLLECT_TTL_MS = 10_000;
160
+ /**
161
+ * When the collectors last finished, on the same clock `render` was given.
162
+ *
163
+ * `undefined` and not 0, because "never collected" and "collected at time
164
+ * zero" have to be different: a caller injecting a clock — a test, or
165
+ * anything replaying — would otherwise find a fresh registry believing it had
166
+ * just collected, and the first scrape would return empty gauges.
167
+ */
168
+ #collectedAt;
169
+ #collecting;
170
+ /**
171
+ * Run the collectors, at most one run at a time and at most one per window.
172
+ *
173
+ * The single-flight part matters as much as the cache: two scrapes arriving
174
+ * together used to start two full sweeps, so concurrency multiplied the cost
175
+ * that the cache alone would bound only in sequence.
176
+ */
177
+ async #collectOnce(now) {
178
+ if (this.#collecting !== undefined)
179
+ return this.#collecting;
180
+ if (this.#collectedAt !== undefined && now - this.#collectedAt < _a.COLLECT_TTL_MS)
181
+ return;
145
182
  // A collector that fails must not blank the whole exposition: the metrics
146
183
  // that still work are how you find out which one broke.
147
- await Promise.allSettled(this.#collectors.map((fn) => fn()));
184
+ this.#collecting = Promise.allSettled(this.#collectors.map((fn) => fn())).then(() => {
185
+ // Stamped with the clock this call was given, never with Date.now().
186
+ // Mixing the two means an injected clock is compared against a real one,
187
+ // and the window is then either always open or never.
188
+ //
189
+ // On completion rather than on entry, so a sweep slower than the window
190
+ // does not have the next one queued behind it the instant it finishes.
191
+ this.#collectedAt = now;
192
+ this.#collecting = undefined;
193
+ });
194
+ return this.#collecting;
195
+ }
196
+ async render(now = Date.now()) {
197
+ await this.#collectOnce(now);
148
198
  const lines = [];
149
199
  for (const metric of this.#metrics) {
150
200
  lines.push(`# HELP ${metric.name} ${metric.help}`);
@@ -154,6 +204,7 @@ export class Registry {
154
204
  return `${lines.join('\n')}\n`;
155
205
  }
156
206
  }
207
+ _a = Registry;
157
208
  /**
158
209
  * The metrics docs/config.md requires.
159
210
  *
@@ -170,10 +221,41 @@ export function createMetrics(registry) {
170
221
  ])),
171
222
  searchResults: registry.register(new Counter('nacre_search_results_total', 'Results returned, by layer')),
172
223
  aclDenials: registry.register(new Counter('nacre_acl_denials_total', 'Access denials, by reason')),
224
+ // Labelled by what was presented, never by why it failed. The 401 itself
225
+ // carries one message for every reason so a caller cannot tell an expired
226
+ // token from a forged one; a `reason` label would hand that distinction
227
+ // back through an endpoint that is unauthenticated by default.
228
+ //
229
+ // `kind="jwt"` is the series a key rotation moves, and it is why this
230
+ // exists: there is no dual-key window, so every outstanding access token
231
+ // fails at once and an operator has to watch that drain. `kind="service_key"`
232
+ // staying flat through the same window is the check that the rotation hit
233
+ // only what it was meant to.
234
+ authFailures: registry.register(new Counter('nacre_auth_failures_total', 'Rejected credentials, by the kind presented: missing, jwt, service_key. Never by reason — the 401 is deliberately one answer')),
173
235
  aclPropagationLag: registry.register(new Gauge('nacre_acl_propagation_lag_seconds', 'Age in seconds of the oldest document, per organization, whose ACL tags predate the current groups_version. Alert on max() over this. The only external evidence that invariant I4 holds')),
174
236
  ingestDuration: registry.register(new Histogram('nacre_ingest_duration_seconds', 'Indexing latency, by stage')),
175
237
  documents: registry.register(new Gauge('nacre_documents_total', 'Documents, by organization and status')),
238
+ // Specified in docs/config.md and registered by nothing until there was a
239
+ // reindex to measure.
240
+ //
241
+ // One series per layer that has ever been reindexed, and no series for the
242
+ // rest — a layer nobody has migrated has no progress to report, and
243
+ // inventing a zero for it would mean every layer in the installation
244
+ // permanently reads "reindex started, gone nowhere".
245
+ //
246
+ // With no reindexes at all the registry still emits a bare
247
+ // `nacre_reindex_progress_ratio 0`, because an unlabelled gauge with no
248
+ // values renders that way here. That is the "nothing to report" marker
249
+ // rather than a layer at zero: a real layer always carries a `layer` label.
250
+ reindexProgress: registry.register(new Gauge('nacre_reindex_progress_ratio', 'How far a layer is through a reindex, 0 to 1. Absent when no reindex has run; 0 means one started and has moved nothing')),
176
251
  tombstonesPending: registry.register(new Gauge('nacre_tombstones_pending_total', 'Deleted documents whose vectors are not yet purged. Climbing means garbage collection is losing')),
252
+ // A gauge and not a counter, because the question an operator has is "how
253
+ // much disk is a finished migration still holding", not "how many have I
254
+ // reclaimed since this process started". Each one retained is a full copy
255
+ // of an organization's vectors, so a number that stays high is the signal —
256
+ // and a number that never falls means the sweep is not running at all,
257
+ // which is the state this whole table replaced.
258
+ collectionsRetired: registry.register(new Gauge('nacre_collections_retired_total', 'Superseded collections still held for the reindex rollback window, by organization. Each is a full copy of that organization vectors')),
177
259
  };
178
260
  }
179
261
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ -- Resolving a credential cannot be scoped to an organization, because the
2
+ -- credential is what says which organization it is.
3
+ --
4
+ -- `PostgresServiceKeys.resolve` reads `service_accounts` by key prefix across
5
+ -- every tenant for exactly that reason. It has never worked anywhere the
6
+ -- documentation says to deploy it: `org_isolation` calls
7
+ -- `current_setting('app.current_org')` in its one-argument form, which raises
8
+ -- `unrecognized configuration parameter` rather than returning null, so the
9
+ -- query fails outright on any connection that is subject to row-level
10
+ -- security. It works in development only because development connects as a
11
+ -- superuser — the one configuration `docs/config.md` explicitly forbids:
12
+ --
13
+ -- $ psql "postgres://nacre_app@…" -c "SELECT id, org_id FROM service_accounts
14
+ -- WHERE key_prefix = '…'"
15
+ -- ERROR: unrecognized configuration parameter "app.current_org"
16
+ --
17
+ -- So every service account key stops working the moment an operator follows
18
+ -- the rule about not connecting as a superuser. Nothing failed in CI, because
19
+ -- CI connects as `postgres` too.
20
+ --
21
+ -- ─── the fix, and why it is a policy rather than a bypass ───
22
+ --
23
+ -- The obvious repair is a SECURITY DEFINER function, and it does not work
24
+ -- here: these tables carry FORCE ROW LEVEL SECURITY, which applies to the
25
+ -- owner as well, so a function running as the owner is still filtered. The
26
+ -- alternative — a role with BYPASSRLS — buys a permanent hole in the isolation
27
+ -- model to solve a problem that lasts one query.
28
+ --
29
+ -- Row-level security is the mechanism, so this uses it. A second policy admits
30
+ -- the lookup, and only while `app.authenticating` is set on the connection.
31
+ -- Policies for the same command are OR'd, so the org-scoped policy is
32
+ -- unchanged for every other caller.
33
+ --
34
+ -- The guard on `org_isolation` is what makes that safe. Without it Postgres
35
+ -- may evaluate the org-scoped expression anyway and raise before the second
36
+ -- policy is ever consulted; `CASE` short-circuits, so the raising call is not
37
+ -- reached while authenticating. It is deliberately left raising in every other
38
+ -- case: a query that forgets `withOrg` should fail loudly rather than quietly
39
+ -- return nothing, and "quietly returns nothing" is indistinguishable from a
40
+ -- permission working correctly, which is the hardest kind of bug to see.
41
+
42
+ ALTER POLICY org_isolation ON service_accounts
43
+ USING (
44
+ CASE
45
+ WHEN current_setting('app.authenticating', true) = 'on' THEN false
46
+ ELSE org_id = current_setting('app.current_org')::uuid
47
+ END
48
+ );
49
+
50
+ -- SELECT only. Authentication reads a credential; it never writes one, and a
51
+ -- policy that admitted INSERT or UPDATE here would let the one path that runs
52
+ -- without an organization create rows in any of them.
53
+ CREATE POLICY authenticating ON service_accounts
54
+ FOR SELECT
55
+ USING (current_setting('app.authenticating', true) = 'on');
56
+
57
+ -- `users` gets neither, and that is a decision rather than an omission. Login
58
+ -- resolves the organization from `organizations` — which has no row-level
59
+ -- security, because an organization is not inside one — and then reads the
60
+ -- user through `withOrg` like everything else. There is no cross-tenant read
61
+ -- on the login path, so there is nothing here to open.
62
+
63
+ -- ─────────────────────── the worker role that was only ever a comment ───────────────────────
64
+ --
65
+ -- `0001_init.sql` says: "Background worker jobs run under a separate role with
66
+ -- BYPASSRLS." No migration ever created it. The worker claims documents across
67
+ -- every tenant — that is what a queue is — and those queries run on a raw
68
+ -- connection with no organization set, so they raise the same
69
+ -- `unrecognized configuration parameter` as the credential lookup did:
70
+ --
71
+ -- $ psql "postgres://nacre_app@…" -c "SELECT d.id, d.org_id FROM documents d
72
+ -- JOIN organizations o ON o.id = d.org_id"
73
+ -- ERROR: unrecognized configuration parameter "app.current_org"
74
+ --
75
+ -- So indexing stops entirely on a deployment that connects as anything but a
76
+ -- superuser. The Compose stack has never shown it, because POSTGRES_USER makes
77
+ -- the connecting role a superuser — the configuration docs/config.md tells
78
+ -- operators not to use.
79
+ --
80
+ -- BYPASSRLS rather than another policy flag, and the difference from the
81
+ -- lookup above is worth being explicit about: authentication reads one column
82
+ -- set from one table and never writes, so a narrow SELECT policy fits it. The
83
+ -- worker reads and writes documents, chunks and layers across every tenant, so
84
+ -- a flag admitting all of that is BYPASSRLS with a worse name and a longer
85
+ -- audit trail. The honest thing is to say so in the role.
86
+ --
87
+ -- Which is also why the worker names org_id explicitly in every query it makes
88
+ -- afterwards: with the second line of defence off, the first one is all there
89
+ -- is.
90
+
91
+ DO $$
92
+ BEGIN
93
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'nacre_worker') THEN
94
+ CREATE ROLE nacre_worker NOLOGIN BYPASSRLS;
95
+ ELSIF NOT (SELECT rolbypassrls FROM pg_roles WHERE rolname = 'nacre_worker') THEN
96
+ ALTER ROLE nacre_worker BYPASSRLS;
97
+ END IF;
98
+ EXCEPTION WHEN insufficient_privilege THEN
99
+ -- Creating a BYPASSRLS role needs a superuser, and the role running
100
+ -- migrations may not be one. Failing here with the statement to run is the
101
+ -- whole point: the alternative is a worker that starts, claims nothing,
102
+ -- and reports itself healthy.
103
+ RAISE EXCEPTION USING
104
+ MESSAGE = 'nacre_worker could not be created: creating a BYPASSRLS role requires a superuser.',
105
+ HINT = 'Run this once as a superuser, then re-run migrations: '
106
+ 'CREATE ROLE nacre_worker NOLOGIN BYPASSRLS; '
107
+ 'GRANT nacre_worker TO <the role in NACRE_PG_URL>;';
108
+ END
109
+ $$;
110
+
111
+ GRANT USAGE ON SCHEMA public TO nacre_worker;
112
+ GRANT SELECT, INSERT, UPDATE, DELETE ON
113
+ organizations, users, groups, group_members, service_accounts,
114
+ embedding_providers, workspaces, layers, documents, chunks, grants
115
+ TO nacre_worker;
116
+ GRANT SELECT, INSERT ON audit_events TO nacre_worker;
117
+ REVOKE UPDATE, DELETE ON audit_events FROM nacre_worker;
118
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO nacre_worker;
119
+
120
+ -- The application role has to be a member to `SET LOCAL ROLE` to it. Granted
121
+ -- rather than assumed: a deployment that manages roles externally will already
122
+ -- have done this, and one that does not would otherwise get a worker that
123
+ -- connects successfully and cannot claim anything.
124
+ GRANT nacre_worker TO nacre_app;
@@ -0,0 +1,73 @@
1
+ -- Refresh tokens, for the login flow.
2
+ --
3
+ -- ─── stored hashed, like every other credential here ───
4
+ --
5
+ -- The column holds a SHA-256 of the token and never the token. A refresh token
6
+ -- outlives an access token by a month, so a database dump that contained them
7
+ -- would be a month of sessions rather than a list of identifiers.
8
+ --
9
+ -- ─── rotation, and what reuse means ───
10
+ --
11
+ -- Every refresh issues a new token and marks the old one used. A used token
12
+ -- presented again is not a client bug worth tolerating: the legitimate holder
13
+ -- has already exchanged it, so a second presentation means two parties hold the
14
+ -- same token, and one of them stole it. There is no way to tell which, so the
15
+ -- whole family is revoked and both are made to sign in again. Annoying once
16
+ -- beats a silent, indefinite session for whoever took it.
17
+ --
18
+ -- `family_id` is what makes that possible: it is the same value for every token
19
+ -- descended from one login, so revoking a family ends that session and no other.
20
+
21
+ CREATE TABLE refresh_tokens (
22
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
23
+ org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
24
+ user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
25
+ -- Not unique across the table by accident: it is unique because the token
26
+ -- is 32 bytes from the CSPRNG, and the constraint says so rather than
27
+ -- leaving a collision to become two users sharing a session.
28
+ token_hash text NOT NULL UNIQUE,
29
+ family_id uuid NOT NULL,
30
+ issued_at timestamptz NOT NULL DEFAULT now(),
31
+ expires_at timestamptz NOT NULL,
32
+ used_at timestamptz,
33
+ revoked_at timestamptz
34
+ );
35
+
36
+ -- The lookup on every refresh: by hash, which is also the uniqueness above.
37
+ -- The family index is for revocation, which touches every row of one family.
38
+ CREATE INDEX refresh_tokens_family ON refresh_tokens (family_id) WHERE revoked_at IS NULL;
39
+ -- Expired rows are deleted by a sweep rather than kept forever; ordering by
40
+ -- expiry is what makes that sweep cheap.
41
+ CREATE INDEX refresh_tokens_expiry ON refresh_tokens (expires_at);
42
+
43
+ ALTER TABLE refresh_tokens ENABLE ROW LEVEL SECURITY;
44
+ ALTER TABLE refresh_tokens FORCE ROW LEVEL SECURITY;
45
+
46
+ -- Scoped like everything else. A refresh token names the organization it was
47
+ -- issued for, and the endpoint that redeems one has already resolved that
48
+ -- organization from the token's own row — through the authenticating policy
49
+ -- below, which is the same mechanism service account keys use and for the same
50
+ -- reason: the credential is what says which tenant it belongs to.
51
+ CREATE POLICY org_isolation ON refresh_tokens
52
+ USING (
53
+ CASE
54
+ WHEN current_setting('app.authenticating', true) = 'on' THEN false
55
+ ELSE org_id = current_setting('app.current_org')::uuid
56
+ END
57
+ );
58
+
59
+ CREATE POLICY authenticating ON refresh_tokens
60
+ FOR SELECT
61
+ USING (current_setting('app.authenticating', true) = 'on');
62
+
63
+ GRANT SELECT, INSERT, UPDATE, DELETE ON refresh_tokens TO nacre_app;
64
+ GRANT SELECT, INSERT, UPDATE, DELETE ON refresh_tokens TO nacre_worker;
65
+
66
+ -- ─── the password column gets a check it never had ───
67
+ --
68
+ -- `users.password_hash` has existed since 0001 and nothing has ever written to
69
+ -- it. Now that something does, an empty string must not be storable: it is
70
+ -- falsy in application code and truthy in SQL, which is exactly the shape of
71
+ -- difference that becomes an account anyone can sign into.
72
+ ALTER TABLE users ADD CONSTRAINT users_password_hash_not_blank
73
+ CHECK (password_hash IS NULL OR length(password_hash) >= 16);
@@ -0,0 +1,22 @@
1
+ -- Pin the search_path on the one SECURITY DEFINER function.
2
+ --
3
+ -- `0003` made `bump_groups_version()` SECURITY DEFINER and did not pin its
4
+ -- search_path. The body's unqualified `organizations` therefore resolves
5
+ -- through the *caller's* search_path, and any role may create objects in
6
+ -- `pg_temp`. A caller who shadows the table stops `groups_version` advancing —
7
+ -- so revocations never propagate, while `nacre_acl_propagation_lag_seconds`
8
+ -- reports a healthy zero because nothing is behind a version that never moved.
9
+ -- A silent failure of invariant 4, with its own evidence metric confirming
10
+ -- health.
11
+ --
12
+ -- It runs as the migration owner, which `docs/config.md` allows to be a
13
+ -- superuser, so the ceiling is high. Reaching it needs database access as
14
+ -- `nacre_app` — defence in depth rather than a remote hole — which is exactly
15
+ -- what the second line of defence is for in a product whose first line is a
16
+ -- permission model.
17
+ --
18
+ -- A separate migration rather than an edit to 0003: migrations are
19
+ -- forward-only and their checksums are re-verified on every run, so editing an
20
+ -- applied file makes the next `migrate` fail on every existing database.
21
+
22
+ ALTER FUNCTION bump_groups_version() SET search_path = pg_catalog, public;
@@ -0,0 +1,36 @@
1
+ -- A lease on the two background sweeps, so more workers do more work.
2
+ --
3
+ -- `claimStale` (retag) and `claimPurgeable` (garbage collection) both selected
4
+ -- the oldest N rows with no lock and no claim marker. Every replica therefore
5
+ -- selected the **same** N rows and did the same work: throughput stayed at one
6
+ -- worker's rate however many were running, while the load on Qdrant multiplied
7
+ -- by the replica count.
8
+ --
9
+ -- That matters beyond efficiency. The documented response to a climbing
10
+ -- `nacre_acl_propagation_lag_seconds` is to look at the worker — and scaling it
11
+ -- out, the obvious move, did nothing at all.
12
+ --
13
+ -- `FOR UPDATE SKIP LOCKED` alone does not fix it. The lock lives until the
14
+ -- transaction commits, and these transactions end the moment the SELECT
15
+ -- returns — the actual work (a Qdrant round trip) happens afterwards, outside
16
+ -- any transaction. So two replicas polling a second apart still collide.
17
+ --
18
+ -- ─── one column, because the two states are mutually exclusive ───
19
+ --
20
+ -- A document is either live and behind its organization's version (retag), or
21
+ -- deleted and not yet purged (garbage collection). `deleted_at IS NULL` decides
22
+ -- which, and nothing is ever both, so one claim column serves both sweeps.
23
+ --
24
+ -- Leased rather than owned, exactly like `documents.claimed_at`: a worker that
25
+ -- dies mid-sweep must not park a row forever. `NACRE_INDEX_LEASE` bounds it,
26
+ -- and the claim is only ever an optimisation — both sweeps are idempotent, and
27
+ -- `markTagged`'s version guard and `markPurged`'s `vectors_purged_at IS NULL`
28
+ -- guard remain the things that make a double pass harmless.
29
+
30
+ ALTER TABLE documents ADD COLUMN sweep_claimed_at timestamptz;
31
+
32
+ -- Partial, and on the claim rather than on the whole table: the sweeps read
33
+ -- "unclaimed, or claimed long enough ago to be abandoned", which is a small
34
+ -- slice of a large table.
35
+ CREATE INDEX documents_sweep_claim ON documents (sweep_claimed_at)
36
+ WHERE sweep_claimed_at IS NOT NULL;