@nacre.work/core 0.1.0 → 0.3.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 (78) hide show
  1. package/dist/audit.d.ts +41 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +18 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/authz/cache.d.ts +20 -0
  6. package/dist/authz/cache.d.ts.map +1 -1
  7. package/dist/authz/cache.js +29 -0
  8. package/dist/authz/cache.js.map +1 -1
  9. package/dist/authz/index.d.ts +1 -2
  10. package/dist/authz/index.d.ts.map +1 -1
  11. package/dist/authz/index.js +1 -2
  12. package/dist/authz/index.js.map +1 -1
  13. package/dist/config.d.ts +132 -5
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +439 -22
  16. package/dist/config.js.map +1 -1
  17. package/dist/db/migrate.d.ts +45 -0
  18. package/dist/db/migrate.d.ts.map +1 -1
  19. package/dist/db/migrate.js +61 -0
  20. package/dist/db/migrate.js.map +1 -1
  21. package/dist/extensions.d.ts +298 -0
  22. package/dist/extensions.d.ts.map +1 -0
  23. package/dist/extensions.js +246 -0
  24. package/dist/extensions.js.map +1 -0
  25. package/dist/index.d.ts +16 -6
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +9 -4
  28. package/dist/index.js.map +1 -1
  29. package/dist/lifecycle.d.ts.map +1 -1
  30. package/dist/lifecycle.js +13 -26
  31. package/dist/lifecycle.js.map +1 -1
  32. package/dist/logging.d.ts +46 -0
  33. package/dist/logging.d.ts.map +1 -0
  34. package/dist/logging.js +110 -0
  35. package/dist/logging.js.map +1 -0
  36. package/dist/metrics.d.ts +11 -7
  37. package/dist/metrics.d.ts.map +1 -1
  38. package/dist/metrics.js +32 -7
  39. package/dist/metrics.js.map +1 -1
  40. package/dist/migrations/0014_retired_collections.sql +68 -0
  41. package/dist/migrations/0015_reference_queries.sql +97 -0
  42. package/dist/migrations/0016_drop_acl_tag_cache.sql +62 -0
  43. package/dist/migrations/0017_drop_workspace_admin_role.sql +30 -0
  44. package/dist/migrations/0018_group_members_nulls_not_distinct.sql +46 -0
  45. package/dist/migrations/0019_embedding_provider_write_guard.sql +58 -0
  46. package/dist/multipart.d.ts +55 -0
  47. package/dist/multipart.d.ts.map +1 -0
  48. package/dist/multipart.js +158 -0
  49. package/dist/multipart.js.map +1 -0
  50. package/dist/oauth.d.ts +10 -0
  51. package/dist/oauth.d.ts.map +1 -1
  52. package/dist/oauth.js +10 -0
  53. package/dist/oauth.js.map +1 -1
  54. package/dist/observability.d.ts.map +1 -1
  55. package/dist/observability.js +43 -46
  56. package/dist/observability.js.map +1 -1
  57. package/dist/reindex.d.ts +44 -0
  58. package/dist/reindex.d.ts.map +1 -1
  59. package/dist/reindex.js +49 -0
  60. package/dist/reindex.js.map +1 -1
  61. package/dist/s3.d.ts +111 -0
  62. package/dist/s3.d.ts.map +1 -0
  63. package/dist/s3.js +242 -0
  64. package/dist/s3.js.map +1 -0
  65. package/dist/tsconfig.tsbuildinfo +1 -1
  66. package/dist/vector/query.d.ts +7 -9
  67. package/dist/vector/query.d.ts.map +1 -1
  68. package/dist/vector/query.js +7 -6
  69. package/dist/vector/query.js.map +1 -1
  70. package/dist/vector/search.d.ts +86 -1
  71. package/dist/vector/search.d.ts.map +1 -1
  72. package/dist/vector/search.js +185 -1
  73. package/dist/vector/search.js.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/authz/tags.d.ts +0 -40
  76. package/dist/authz/tags.d.ts.map +0 -1
  77. package/dist/authz/tags.js +0 -48
  78. package/dist/authz/tags.js.map +0 -1
@@ -53,6 +53,62 @@ async function checksum(sql) {
53
53
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(sql));
54
54
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
55
55
  }
56
+ /**
57
+ * Refuse, before applying anything, if the connected role cannot finish.
58
+ *
59
+ * Every tenant table has `FORCE ROW LEVEL SECURITY`, and `FORCE` is what makes
60
+ * the policy apply to the table's **owner** as well. Migrations run as the
61
+ * owner and several of them read a tenant table — 0006 checks for duplicate
62
+ * layer slugs, 0007 backfills a lease column, 0017 rewrites a role, 0018
63
+ * dedupes group members. Each of those evaluates
64
+ * `current_setting('app.current_org')`, which is unset during a migration, so
65
+ * the statement fails with:
66
+ *
67
+ * ERROR: unrecognized configuration parameter "app.current_org"
68
+ *
69
+ * That message names nothing an operator can act on. It says nothing about
70
+ * roles, nothing about row-level security, and it arrives at migration 0006
71
+ * rather than at the start — five migrations of schema already applied.
72
+ *
73
+ * It has never been seen because development connects as a superuser, and a
74
+ * superuser bypasses row-level security entirely. It appears the moment an
75
+ * operator follows `docs/config.md` and stops doing that, which is the same
76
+ * shape as the two subsystems already found this way.
77
+ *
78
+ * So the requirement is stated once, up front, with the SQL that satisfies it.
79
+ * The `WITH ADMIN OPTION` line is in the message even though it is not what is
80
+ * being checked: migration 0008 runs `GRANT nacre_worker TO nacre_app` and
81
+ * plain membership is not enough to grant a role onward. That migration's own
82
+ * hint says to grant membership, which does not fix it — and 0008 has been
83
+ * applied everywhere, so its text is checksummed and cannot be corrected in
84
+ * place. Naming the whole block here is what stops an operator satisfying one
85
+ * requirement and meeting the next one on the following run.
86
+ *
87
+ * Only called when there is something to apply, so re-running `migrate` on an
88
+ * up-to-date database stays a no-op whatever role it connects as.
89
+ */
90
+ export async function requireMigrationPrivileges(client) {
91
+ const { rows } = await client.query('SELECT rolsuper AS superuser, rolbypassrls AS bypassrls FROM pg_roles WHERE rolname = current_user');
92
+ const role = rows[0];
93
+ if (role === undefined || role.superuser || role.bypassrls)
94
+ return;
95
+ throw new Error('the role running migrations can neither bypass row-level security nor is a ' +
96
+ 'superuser, and several migrations read tenant tables. Every one of those ' +
97
+ 'tables has FORCE ROW LEVEL SECURITY, which applies the policy to the owner ' +
98
+ 'too, and the policy reads app.current_org — unset during a migration. The ' +
99
+ 'migration would fail partway through with "unrecognized configuration ' +
100
+ 'parameter \\"app.current_org\\"", which names none of this.\n\n' +
101
+ 'Run this once as a superuser:\n\n' +
102
+ ' ALTER ROLE <the role in NACRE_PG_URL> BYPASSRLS;\n' +
103
+ ' CREATE ROLE nacre_worker NOLOGIN BYPASSRLS; -- if it does not exist\n' +
104
+ ' GRANT nacre_worker TO <the role in NACRE_PG_URL> WITH ADMIN OPTION;\n\n' +
105
+ 'WITH ADMIN OPTION is required and plain membership is not: migration 0008 ' +
106
+ 'grants nacre_worker onward to nacre_app, and only a member holding ADMIN ' +
107
+ 'may do that.\n\n' +
108
+ 'This role is the one that owns the tables and runs migrations. It is not ' +
109
+ 'the role the application connects as — that one must not bypass row-level ' +
110
+ 'security and must not be able to create tables.');
111
+ }
56
112
  /**
57
113
  * Apply every migration that has not been applied yet.
58
114
  *
@@ -79,6 +135,11 @@ export async function migrate(connectionString, migrations = loadMigrations()) {
79
135
  await client.query(LEDGER);
80
136
  const { rows } = await client.query('SELECT name, checksum FROM schema_migrations');
81
137
  const ledger = new Map(rows.map((r) => [r.name, r.checksum]));
138
+ // Only when there is work to do. A re-run against an up-to-date database
139
+ // applies nothing, so the privileges it would need are not needed.
140
+ if (migrations.some((m) => !ledger.has(m.name))) {
141
+ await requireMigrationPrivileges(client);
142
+ }
82
143
  for (const migration of migrations) {
83
144
  const sum = await checksum(migration.sql);
84
145
  const recorded = ledger.get(migration.name);
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../db/migrate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../db/migrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,298 @@
1
+ import type { OrgRole, Permission, Principal } from './types.js';
2
+ import type { AccessPlan, ResolveInput } from './authz/resolve.js';
3
+ /**
4
+ * The points a commercial module plugs into, and the loader that lets one do so
5
+ * without this repository knowing it exists.
6
+ *
7
+ * Four of them — `registerAuthProvider`, `registerAuthzResolver`,
8
+ * `registerAuditSink`, `mountAdminRoutes` — `nacre-enterprise` named before
9
+ * there was a module to write. The fifth, `registerIngestGate`, is the other
10
+ * direction: a point the core declares for a capability the schema always had
11
+ * and nothing enforced (`max_documents`), designed so the open half is complete
12
+ * and correct with no gate registered.
13
+ *
14
+ * `nacre-enterprise` has named these since before there was a module to write —
15
+ * `registerAuthProvider`, `registerAuthzResolver`, `registerAuditSink`,
16
+ * `mountAdminRoutes` — and nothing here declared them. That document was the
17
+ * only place they existed, which made "plug into points declared by
18
+ * `@nacre.work/core`" a sentence about a thing that was not there.
19
+ *
20
+ * ## The boundary is the reason for the shape
21
+ *
22
+ * The core's CI fails if anything under `packages/` or `services/` mentions the
23
+ * enterprise package by name, and that check is what keeps the open half
24
+ * honest: it cannot import the closed half even by accident. So a module cannot
25
+ * be wired in by `main.ts` importing it. It is named in configuration
26
+ * (`NACRE_MODULES`), imported dynamically here, and registers itself.
27
+ *
28
+ * A registry is therefore not a style preference — it is the only shape that
29
+ * satisfies "the core must not know this repository exists" while still letting
30
+ * a deployment run both halves in one process.
31
+ *
32
+ * ## What the registry refuses
33
+ *
34
+ * **Registration is only open while modules are loading.** Anything registered
35
+ * afterwards would be registered after the surfaces were composed, so it would
36
+ * be configured, look present, and never be consulted — the failure this file
37
+ * exists to prevent, one level up.
38
+ *
39
+ * **One resolver, and a second is an error rather than a replacement.** Two
40
+ * answers to "who decides access" is not something to settle by precedence: the
41
+ * loser stays loaded, appears to be in force, and is not. The same argument
42
+ * `loadJwtKeys` makes about a secret and a key reference.
43
+ *
44
+ * Sinks, providers and routes are lists, because more than one of each is a
45
+ * coherent thing to want and none of them decides access on its own.
46
+ *
47
+ * ## The core is complete with nothing plugged in
48
+ *
49
+ * Every default here is the behaviour this repository already had: the built-in
50
+ * resolver, no extra credential types, no extra sinks, no extra routes. A
51
+ * deployment that sets no modules gets exactly the code path it got before this
52
+ * file existed, and the tests that prove the permission model still run against
53
+ * the built-in resolver because that is what is registered.
54
+ */
55
+ /**
56
+ * A replacement for permission evaluation.
57
+ *
58
+ * **This is the only interface in this repository that can break the six
59
+ * invariants from outside it.** A module that implements it takes over the
60
+ * question every other rule depends on, so `nacre-enterprise/CLAUDE.md` binds
61
+ * it to the same document the built-in one is written from: it does not relax
62
+ * rule 6, and it does not return `403` where the core returns `404`.
63
+ *
64
+ * The signature is deliberately the built-in one. A resolver that needed extra
65
+ * inputs would be a change to `ResolveInput` here, in the open half, reviewed
66
+ * like any other change to the model — not a private extension of it.
67
+ */
68
+ export interface AuthzResolver {
69
+ resolve(input: ResolveInput, permission: Permission): AccessPlan;
70
+ }
71
+ /** What a credential resolves to. Structurally the API's `AuthContext`. */
72
+ export interface ResolvedPrincipal {
73
+ /** From the credential, never from the request. Invariant 1. */
74
+ readonly orgId: string;
75
+ readonly principal: Principal;
76
+ readonly role: OrgRole;
77
+ }
78
+ /**
79
+ * A credential type the core does not understand.
80
+ *
81
+ * Consulted only after the built-in paths decline, and only for a bearer token
82
+ * that is not a JWT this deployment can verify and not a `nacre_sk_` key — so a
83
+ * provider cannot shadow either. `undefined` means "not mine", which is not the
84
+ * same as "invalid": every refusal still ends in the one `401` with the one
85
+ * message, because distinguishing them is what invariant 4 forbids one level
86
+ * down.
87
+ */
88
+ export interface AuthProvider {
89
+ /** For the startup line and for errors. Not shown to a caller. */
90
+ readonly name: string;
91
+ authenticate(credential: string): Promise<ResolvedPrincipal | undefined>;
92
+ }
93
+ /**
94
+ * One recorded access, as handed to a sink.
95
+ *
96
+ * Declared here rather than in `packages/api` because a sink is registered from
97
+ * outside both, and a second structurally-identical definition is two things
98
+ * that have to agree with nothing making them.
99
+ */
100
+ export interface AuditEvent {
101
+ readonly orgId: string;
102
+ readonly actor: string;
103
+ readonly action: string;
104
+ readonly result: 'allow' | 'deny' | 'error';
105
+ readonly detail: Record<string, unknown>;
106
+ readonly requestId: string;
107
+ /** Which surface the call came in on. Defaults to `api` at the writer. */
108
+ readonly surface?: 'api' | 'mcp' | 'admin' | 'system';
109
+ /** What the call was about, as `docs/audit.md` specifies it. */
110
+ readonly target?: Record<string, unknown>;
111
+ }
112
+ /**
113
+ * Somewhere else an event goes, **in addition to** the table.
114
+ *
115
+ * Never instead of it. The journal is append-only and is what an auditor reads;
116
+ * a forwarder that could replace it would make the guarantee depend on a
117
+ * network hop. `docs/audit.md`'s SIEM export is this.
118
+ */
119
+ export interface AuditSink {
120
+ readonly name: string;
121
+ write(event: AuditEvent): Promise<void>;
122
+ }
123
+ /** A request as an admin route sees it — no framework, no server type. */
124
+ export interface AdminRequest {
125
+ readonly method: string;
126
+ readonly path: string;
127
+ /** Capture groups from `pattern`, in order. */
128
+ readonly params: readonly string[];
129
+ readonly query: URLSearchParams;
130
+ readonly body: unknown;
131
+ /** Already authenticated. A route never sees an anonymous request. */
132
+ readonly auth: ResolvedPrincipal;
133
+ }
134
+ export interface AdminResponse {
135
+ readonly status: number;
136
+ readonly body?: unknown;
137
+ }
138
+ /**
139
+ * A route a module adds under `/v1/admin/`.
140
+ *
141
+ * Confined to that prefix by the mounter rather than by convention: a module
142
+ * that could mount `/v1/search` would be replacing a surface the open half
143
+ * documents and tests, which is a different thing from extending it.
144
+ */
145
+ export interface AdminRoute {
146
+ readonly method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
147
+ /** Matched against the path. Must be anchored under `/v1/admin/`. */
148
+ readonly pattern: RegExp;
149
+ handle(request: AdminRequest): Promise<AdminResponse>;
150
+ }
151
+ /**
152
+ * What an ingest gate is told about a document before it is stored.
153
+ *
154
+ * Metadata, never the bytes. The gate decides whether this document may be
155
+ * accepted at all — a quota, a suspension, a per-source rule — and for that it
156
+ * needs to know the tenant, the layer and how big the thing is, not its
157
+ * content. Keeping the content out is the same discipline `rejectTenantOverride`
158
+ * follows: a document body does not belong in something that gets passed around,
159
+ * logged and put into a refusal message.
160
+ */
161
+ export interface IngestContext {
162
+ /** From the credential. Invariant 1. */
163
+ readonly orgId: string;
164
+ readonly layerId: string;
165
+ readonly principal: Principal;
166
+ readonly role: OrgRole;
167
+ readonly externalId: string;
168
+ /** The document's size in bytes, for a gate that bounds volume. */
169
+ readonly bytes: number;
170
+ }
171
+ /**
172
+ * A gate's answer. Admit, or refuse with a reason and the 4xx to answer with.
173
+ *
174
+ * A refusal is **not** a `404`: the caller has already been shown to hold
175
+ * `write` on the layer by the time a gate runs, so the layer's existence is not
176
+ * a secret this hides — unlike the write check itself, which returns `404` for
177
+ * absent and unpermitted alike. A quota or a suspension is a real answer a
178
+ * caller with access is entitled to, so it defaults to `403` and carries a
179
+ * reason.
180
+ */
181
+ export type IngestVerdict = {
182
+ readonly admit: true;
183
+ } | {
184
+ readonly admit: false;
185
+ readonly status?: number;
186
+ readonly reason: string;
187
+ };
188
+ /**
189
+ * A check run before a document is accepted, in addition to the permission
190
+ * check the core already does.
191
+ *
192
+ * A list, and every gate must admit — one deny refuses the ingest, with that
193
+ * gate's reason. Gates decide admission, not access: a gate cannot *grant* an
194
+ * ingest the permission model would refuse, because it runs only after
195
+ * `write` has been established. It can only subtract, which is why more than one
196
+ * is coherent and why the default — no gates — is the open core accepting every
197
+ * document a caller may write, exactly as it did before this point existed.
198
+ *
199
+ * `max_documents`, which the schema has carried since 0001 and nothing enforced,
200
+ * is the first user: a commercial `tenancy` gate that counts a tenant's live
201
+ * documents and refuses over the quota.
202
+ */
203
+ export interface IngestGate {
204
+ readonly name: string;
205
+ admit(context: IngestContext): Promise<IngestVerdict>;
206
+ }
207
+ export declare function registerAuthzResolver(resolver: AuthzResolver): void;
208
+ export declare function registerAuthProvider(provider: AuthProvider): void;
209
+ export declare function registerAuditSink(sink: AuditSink): void;
210
+ export declare function registerIngestGate(gate: IngestGate): void;
211
+ export declare const ADMIN_PREFIX = "/v1/admin/";
212
+ export declare function mountAdminRoutes(...routes: readonly AdminRoute[]): void;
213
+ /**
214
+ * Import the named modules and let them register, then close registration.
215
+ *
216
+ * Dynamic by name, from configuration, which is what keeps the enterprise
217
+ * package's name out of this repository's source — the `boundary` job greps for
218
+ * it, and a static import would be the leak that job exists to catch.
219
+ *
220
+ * A module that cannot be imported is a startup failure, not a warning. The
221
+ * whole point of naming one is that a deployment is paying for what it does,
222
+ * and starting without it would silently be a different product.
223
+ */
224
+ export declare function loadModules(names: readonly string[], importer?: (name: string) => Promise<unknown>): Promise<void>;
225
+ /**
226
+ * The resolver in force.
227
+ *
228
+ * The built-in one unless a module replaced it, which is what makes the core
229
+ * complete with nothing plugged in. Read per call rather than captured, so
230
+ * there is no window in which a surface holds a stale one.
231
+ */
232
+ export declare function activeResolver(): AuthzResolver;
233
+ export declare const authProviders: () => readonly AuthProvider[];
234
+ export declare const auditSinks: () => readonly AuditSink[];
235
+ export declare const adminRoutes: () => readonly AdminRoute[];
236
+ /** A refused ingest, as `admitIngest` reports it. */
237
+ export interface IngestRefusal {
238
+ readonly module: string;
239
+ readonly status: number;
240
+ readonly reason: string;
241
+ }
242
+ /**
243
+ * Run every gate, in order, and report the first refusal.
244
+ *
245
+ * `undefined` is admission: no gates registered, or every one admitted, which
246
+ * is why the open core accepts what a caller may write. The first deny stops the
247
+ * rest — a document refused by one gate is refused, and asking the others would
248
+ * be work whose answer cannot change the outcome. The default status is `403`,
249
+ * so a gate that only sets a reason still refuses with a status a caller with
250
+ * access can act on rather than a `404` that would hide the layer it just wrote
251
+ * a check against.
252
+ */
253
+ export declare function admitIngest(context: IngestContext): Promise<IngestRefusal | undefined>;
254
+ /** Anything that records an event. Structurally the API's `audit` port. */
255
+ export interface AuditWriter {
256
+ write(event: AuditEvent): Promise<void>;
257
+ }
258
+ /**
259
+ * Record the event, then hand it to every sink.
260
+ *
261
+ * A decorator over the port rather than something the Postgres adapter does,
262
+ * which is where it started and which put it one layer too low: forwarding is a
263
+ * property of *an event having been recorded*, not of which adapter recorded it.
264
+ * A second adapter — or a surface handed a different port — would have been a
265
+ * SIEM silently missing a class of events, with nothing to notice it by.
266
+ *
267
+ * The table first and never the reverse. The journal is what an auditor reads
268
+ * and what the append-only grant protects, so a forwarder must not be able to
269
+ * make a row conditional on a network hop succeeding.
270
+ *
271
+ * A sink that throws does not fail the request. It is a copy of something
272
+ * already durably recorded, and turning a collector outage into a 500 on every
273
+ * authenticated call would take the installation down to protect a duplicate.
274
+ *
275
+ * Applying it twice is a no-op, so a construction site that wraps a port
276
+ * already wrapped does not double every event in the deployment's SIEM.
277
+ */
278
+ export declare function withAuditSinks<T extends AuditWriter>(port: T, onError?: (sink: string, event: AuditEvent, error: unknown) => void): T;
279
+ /** What was loaded, for the startup line. An operator needs to see it. */
280
+ export declare function loadedExtensions(): {
281
+ resolver: string | null;
282
+ providers: string[];
283
+ sinks: string[];
284
+ routes: number;
285
+ gates: string[];
286
+ };
287
+ /**
288
+ * Empty the registry. **Tests only**, and named so that is unmistakable.
289
+ *
290
+ * A process does not unload a module, so there is no production reason to call
291
+ * this — but a test file that registered one would otherwise leak it into every
292
+ * test after it, which is exactly the shape of failure a global registry earns
293
+ * if nobody gives it a way out.
294
+ */
295
+ export declare function resetExtensionsForTests(): void;
296
+ /** Open registration without importing anything. **Tests only.** */
297
+ export declare function withLoadingModuleForTests<T>(name: string, fn: () => T): T;
298
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../extensions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,246 @@
1
+ import { resolve as builtInResolve } from './authz/resolve.js';
2
+ const registry = { resolver: undefined, providers: [], sinks: [], routes: [], gates: [] };
3
+ /** Which module is registering, or `undefined` when registration is closed. */
4
+ let loading;
5
+ class ExtensionError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'ExtensionError';
9
+ }
10
+ }
11
+ function mustBeLoading(what) {
12
+ if (loading === undefined) {
13
+ throw new ExtensionError(`${what} was registered outside module loading. Extensions are read once, ` +
14
+ 'when the surfaces are composed, so anything registered afterwards would ' +
15
+ 'be configured and never consulted. Register from the module body, which ' +
16
+ 'runs during loadModules().');
17
+ }
18
+ return loading;
19
+ }
20
+ export function registerAuthzResolver(resolver) {
21
+ const module = mustBeLoading('an authorization resolver');
22
+ if (registry.resolver !== undefined) {
23
+ // Refused rather than replaced. See the note at the top: the loser would
24
+ // stay loaded and appear to be in force.
25
+ throw new ExtensionError(`two authorization resolvers were registered — ${registry.resolver.module} ` +
26
+ `and ${module}. There is one answer to who decides access and no order ` +
27
+ 'of precedence worth inventing. Load one.');
28
+ }
29
+ registry.resolver = { module, value: resolver };
30
+ }
31
+ export function registerAuthProvider(provider) {
32
+ registry.providers.push({ module: mustBeLoading('an auth provider'), value: provider });
33
+ }
34
+ export function registerAuditSink(sink) {
35
+ registry.sinks.push({ module: mustBeLoading('an audit sink'), value: sink });
36
+ }
37
+ export function registerIngestGate(gate) {
38
+ registry.gates.push({ module: mustBeLoading('an ingest gate'), value: gate });
39
+ }
40
+ export const ADMIN_PREFIX = '/v1/admin/';
41
+ /**
42
+ * Paths the pattern is asked about directly.
43
+ *
44
+ * Not a proof and not meant as one — the anchor check above is the guarantee.
45
+ * This catches the thing the anchor check cannot see, which is a top-level
46
+ * alternation: `^/v1/admin/x|^/v1/search` starts with the right prefix and
47
+ * matches a documented surface anyway. The list is the surfaces where being
48
+ * wrong costs the most, plus the two near misses on the prefix itself.
49
+ */
50
+ const MUST_NOT_MATCH = [
51
+ '/v1/search',
52
+ '/v1/documents/00000000-0000-0000-0000-000000000000',
53
+ '/v1/grants',
54
+ '/v1/layers',
55
+ '/v1/workspaces',
56
+ '/v1/audit',
57
+ '/v1/auth/login',
58
+ '/v1/health',
59
+ '/.well-known/jwks.json',
60
+ '/v1/admin',
61
+ '/v1/adminish/things',
62
+ ];
63
+ export function mountAdminRoutes(...routes) {
64
+ const module = mustBeLoading('admin routes');
65
+ for (const route of routes) {
66
+ // Checked here rather than trusted. A route outside the prefix would be a
67
+ // module replacing a documented surface rather than adding to one, and the
68
+ // dispatcher would happily serve it.
69
+ //
70
+ // Anchored, not merely containing: `/v1/admin/x` without the `^` matches
71
+ // `/v1/search?q=/v1/admin/x` shaped paths as readily as the intended one,
72
+ // and the whole value of the prefix is that it is a prefix.
73
+ //
74
+ // `\/` is unescaped first, because `/^\/v1\/admin\/x$/.source` is
75
+ // `^\/v1\/admin\/x$` while `new RegExp('^/v1/admin/x').source` is
76
+ // `^/v1/admin/x` — the same pattern, two spellings. A check on the raw
77
+ // source accepts one and refuses the other, which makes it a check on how
78
+ // the author escaped rather than on what the pattern matches. `\/` in a
79
+ // pattern means the character `/` and nothing else, so this rewrite is
80
+ // exact rather than a guess.
81
+ // `g` and `y` make a pattern stateful: `test` and `exec` advance
82
+ // `lastIndex` and the next call starts from there, so the same request
83
+ // path matches or does not depending on what was dispatched before it.
84
+ // Refused rather than worked around by resetting, because a route that
85
+ // only works because the dispatcher remembers to clear a field is a route
86
+ // that breaks the first time someone matches it somewhere else.
87
+ if (route.pattern.global || route.pattern.sticky) {
88
+ throw new ExtensionError(`${module} mounted a route whose pattern carries the ` +
89
+ `${route.pattern.global ? 'g' : 'y'} flag: ${String(route.pattern)}. ` +
90
+ 'A stateful pattern matches or does not depending on what was ' +
91
+ 'matched before it. Drop the flag.');
92
+ }
93
+ const source = route.pattern.source.replaceAll('\\/', '/');
94
+ if (!source.startsWith(`^${ADMIN_PREFIX}`)) {
95
+ throw new ExtensionError(`${module} mounted a route whose pattern is not anchored under ` +
96
+ `${ADMIN_PREFIX}: ${route.pattern.source}. Modules extend the admin ` +
97
+ 'surface; they do not replace the documented ones.');
98
+ }
99
+ for (const path of MUST_NOT_MATCH) {
100
+ if (route.pattern.test(path)) {
101
+ throw new ExtensionError(`${module} mounted a route anchored under ${ADMIN_PREFIX} that also ` +
102
+ `matches ${path}. A pattern can leave the prefix through a ` +
103
+ 'top-level alternation; this one does. Mount one route per path.');
104
+ }
105
+ }
106
+ }
107
+ // Every route checked before any is kept, so a refused batch mounts none of
108
+ // itself. A partial mount would be a module half in force, which is the state
109
+ // this file refuses everywhere else.
110
+ for (const route of routes)
111
+ registry.routes.push({ module, value: route });
112
+ }
113
+ /**
114
+ * Import the named modules and let them register, then close registration.
115
+ *
116
+ * Dynamic by name, from configuration, which is what keeps the enterprise
117
+ * package's name out of this repository's source — the `boundary` job greps for
118
+ * it, and a static import would be the leak that job exists to catch.
119
+ *
120
+ * A module that cannot be imported is a startup failure, not a warning. The
121
+ * whole point of naming one is that a deployment is paying for what it does,
122
+ * and starting without it would silently be a different product.
123
+ */
124
+ export async function loadModules(names, importer = (name) => import(/* @vite-ignore */ name)) {
125
+ for (const name of names) {
126
+ loading = name;
127
+ try {
128
+ await importer(name);
129
+ }
130
+ catch (cause) {
131
+ throw new ExtensionError(`module ${name} could not be loaded: ${String(cause)}`);
132
+ }
133
+ finally {
134
+ loading = undefined;
135
+ }
136
+ }
137
+ }
138
+ /**
139
+ * The resolver in force.
140
+ *
141
+ * The built-in one unless a module replaced it, which is what makes the core
142
+ * complete with nothing plugged in. Read per call rather than captured, so
143
+ * there is no window in which a surface holds a stale one.
144
+ */
145
+ export function activeResolver() {
146
+ return registry.resolver?.value ?? { resolve: builtInResolve };
147
+ }
148
+ export const authProviders = () => registry.providers.map((p) => p.value);
149
+ export const auditSinks = () => registry.sinks.map((s) => s.value);
150
+ export const adminRoutes = () => registry.routes.map((r) => r.value);
151
+ /**
152
+ * Run every gate, in order, and report the first refusal.
153
+ *
154
+ * `undefined` is admission: no gates registered, or every one admitted, which
155
+ * is why the open core accepts what a caller may write. The first deny stops the
156
+ * rest — a document refused by one gate is refused, and asking the others would
157
+ * be work whose answer cannot change the outcome. The default status is `403`,
158
+ * so a gate that only sets a reason still refuses with a status a caller with
159
+ * access can act on rather than a `404` that would hide the layer it just wrote
160
+ * a check against.
161
+ */
162
+ export async function admitIngest(context) {
163
+ for (const gate of registry.gates) {
164
+ const verdict = await gate.value.admit(context);
165
+ if (!verdict.admit) {
166
+ return { module: gate.value.name, status: verdict.status ?? 403, reason: verdict.reason };
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+ const FANNED_OUT = Symbol.for('nacre.auditSinksApplied');
172
+ /**
173
+ * Record the event, then hand it to every sink.
174
+ *
175
+ * A decorator over the port rather than something the Postgres adapter does,
176
+ * which is where it started and which put it one layer too low: forwarding is a
177
+ * property of *an event having been recorded*, not of which adapter recorded it.
178
+ * A second adapter — or a surface handed a different port — would have been a
179
+ * SIEM silently missing a class of events, with nothing to notice it by.
180
+ *
181
+ * The table first and never the reverse. The journal is what an auditor reads
182
+ * and what the append-only grant protects, so a forwarder must not be able to
183
+ * make a row conditional on a network hop succeeding.
184
+ *
185
+ * A sink that throws does not fail the request. It is a copy of something
186
+ * already durably recorded, and turning a collector outage into a 500 on every
187
+ * authenticated call would take the installation down to protect a duplicate.
188
+ *
189
+ * Applying it twice is a no-op, so a construction site that wraps a port
190
+ * already wrapped does not double every event in the deployment's SIEM.
191
+ */
192
+ export function withAuditSinks(port, onError = () => { }) {
193
+ if (port[FANNED_OUT] === true)
194
+ return port;
195
+ const wrapped = {
196
+ write: async (event) => {
197
+ await port.write(event);
198
+ for (const sink of auditSinks()) {
199
+ try {
200
+ await sink.write(event);
201
+ }
202
+ catch (error) {
203
+ onError(sink.name, event, error);
204
+ }
205
+ }
206
+ },
207
+ };
208
+ return Object.assign(Object.create(port), wrapped, { [FANNED_OUT]: true });
209
+ }
210
+ /** What was loaded, for the startup line. An operator needs to see it. */
211
+ export function loadedExtensions() {
212
+ return {
213
+ resolver: registry.resolver?.module ?? null,
214
+ providers: registry.providers.map((p) => `${p.module}:${p.value.name}`),
215
+ sinks: registry.sinks.map((s) => `${s.module}:${s.value.name}`),
216
+ routes: registry.routes.length,
217
+ gates: registry.gates.map((g) => `${g.module}:${g.value.name}`),
218
+ };
219
+ }
220
+ /**
221
+ * Empty the registry. **Tests only**, and named so that is unmistakable.
222
+ *
223
+ * A process does not unload a module, so there is no production reason to call
224
+ * this — but a test file that registered one would otherwise leak it into every
225
+ * test after it, which is exactly the shape of failure a global registry earns
226
+ * if nobody gives it a way out.
227
+ */
228
+ export function resetExtensionsForTests() {
229
+ registry.resolver = undefined;
230
+ registry.providers.length = 0;
231
+ registry.sinks.length = 0;
232
+ registry.routes.length = 0;
233
+ registry.gates.length = 0;
234
+ loading = undefined;
235
+ }
236
+ /** Open registration without importing anything. **Tests only.** */
237
+ export function withLoadingModuleForTests(name, fn) {
238
+ loading = name;
239
+ try {
240
+ return fn();
241
+ }
242
+ finally {
243
+ loading = undefined;
244
+ }
245
+ }
246
+ //# sourceMappingURL=extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../extensions.ts"],"names":[],"mappings":"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"}