@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.
- package/dist/audit.d.ts +41 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +18 -0
- package/dist/audit.js.map +1 -0
- package/dist/authz/cache.d.ts +20 -0
- package/dist/authz/cache.d.ts.map +1 -1
- package/dist/authz/cache.js +29 -0
- package/dist/authz/cache.js.map +1 -1
- package/dist/authz/index.d.ts +1 -2
- package/dist/authz/index.d.ts.map +1 -1
- package/dist/authz/index.js +1 -2
- package/dist/authz/index.js.map +1 -1
- package/dist/config.d.ts +132 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +439 -22
- package/dist/config.js.map +1 -1
- package/dist/db/migrate.d.ts +45 -0
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +61 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/extensions.d.ts +298 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +246 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +13 -26
- package/dist/lifecycle.js.map +1 -1
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +110 -0
- package/dist/logging.js.map +1 -0
- package/dist/metrics.d.ts +11 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +32 -7
- package/dist/metrics.js.map +1 -1
- package/dist/migrations/0014_retired_collections.sql +68 -0
- package/dist/migrations/0015_reference_queries.sql +97 -0
- package/dist/migrations/0016_drop_acl_tag_cache.sql +62 -0
- package/dist/migrations/0017_drop_workspace_admin_role.sql +30 -0
- package/dist/migrations/0018_group_members_nulls_not_distinct.sql +46 -0
- package/dist/migrations/0019_embedding_provider_write_guard.sql +58 -0
- package/dist/multipart.d.ts +55 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +158 -0
- package/dist/multipart.js.map +1 -0
- package/dist/oauth.d.ts +10 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +10 -0
- package/dist/oauth.js.map +1 -1
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +43 -46
- package/dist/observability.js.map +1 -1
- package/dist/reindex.d.ts +44 -0
- package/dist/reindex.d.ts.map +1 -1
- package/dist/reindex.js +49 -0
- package/dist/reindex.js.map +1 -1
- package/dist/s3.d.ts +111 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +242 -0
- package/dist/s3.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vector/query.d.ts +7 -9
- package/dist/vector/query.d.ts.map +1 -1
- package/dist/vector/query.js +7 -6
- package/dist/vector/query.js.map +1 -1
- package/dist/vector/search.d.ts +86 -1
- package/dist/vector/search.d.ts.map +1 -1
- package/dist/vector/search.js +185 -1
- package/dist/vector/search.js.map +1 -1
- package/package.json +1 -1
- package/dist/authz/tags.d.ts +0 -40
- package/dist/authz/tags.d.ts.map +0 -1
- package/dist/authz/tags.js +0 -48
- package/dist/authz/tags.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- The query set a reindex is checked against.
|
|
2
|
+
--
|
|
3
|
+
-- `docs/architecture.md` has carried a step 4 since before there was a reindex
|
|
4
|
+
-- — "the recall check against a reference query set" — and has listed it as not
|
|
5
|
+
-- built for just as long, with the reason: it is "a gate nobody can run without
|
|
6
|
+
-- a query set". This is the query set.
|
|
7
|
+
--
|
|
8
|
+
-- ─── what the gate is for ───
|
|
9
|
+
--
|
|
10
|
+
-- A migration onto a new model succeeds mechanically whether or not the new
|
|
11
|
+
-- model works. Every document gets a shadow vector, the count reaches zero,
|
|
12
|
+
-- `vector_name` moves, and search is now answered by the new slot. If the
|
|
13
|
+
-- provider was misconfigured — the right endpoint behind the wrong model name,
|
|
14
|
+
-- a truncated dimension, an embedder returning near-constant vectors — every
|
|
15
|
+
-- step still reports success and retrieval quietly collapses. There is no
|
|
16
|
+
-- failing test, no alert, and no error anywhere, because nothing failed.
|
|
17
|
+
--
|
|
18
|
+
-- The gate is the only thing in the sequence that asks whether the new model
|
|
19
|
+
-- can still answer, rather than whether the write happened.
|
|
20
|
+
--
|
|
21
|
+
-- ─── why ground truth rather than agreement with the old model ───
|
|
22
|
+
--
|
|
23
|
+
-- The cheaper design compares the new slot's answers against the old slot's and
|
|
24
|
+
-- gates on the overlap. It needs nothing from the operator, and it is wrong:
|
|
25
|
+
-- **a better model disagrees with the worse one it replaces**, so a gate on
|
|
26
|
+
-- agreement blocks exactly the migrations worth making, and passes a new model
|
|
27
|
+
-- that reproduces the old one's mistakes. What an operator means by "search
|
|
28
|
+
-- still works" is against documents they picked, which is a thing only they
|
|
29
|
+
-- have.
|
|
30
|
+
--
|
|
31
|
+
-- So the set is theirs and the gate is off until they write one. That is the
|
|
32
|
+
-- honest arrangement, and it is why this table exists rather than a constant.
|
|
33
|
+
--
|
|
34
|
+
-- ─── external ids, not document ids ───
|
|
35
|
+
--
|
|
36
|
+
-- `expected` names documents the way the operator does. A document deleted and
|
|
37
|
+
-- re-ingested is the same reference; a document id would have changed underneath
|
|
38
|
+
-- them silently and scored as a miss. The resolution to ids happens when the
|
|
39
|
+
-- check runs, and an entry that resolves to nothing **fails the check with that
|
|
40
|
+
-- reason** rather than counting as a miss — a stale reference set and a bad
|
|
41
|
+
-- model are different problems and must not produce the same number.
|
|
42
|
+
|
|
43
|
+
-- Composite keys for the cross-tenant foreign key below, the same shape 0002
|
|
44
|
+
-- gave `groups` and `users`. Without it a row could name a layer in one
|
|
45
|
+
-- organization while claiming another, and only the code writing the insert
|
|
46
|
+
-- would stop it.
|
|
47
|
+
ALTER TABLE layers ADD CONSTRAINT layers_id_org_key UNIQUE (id, org_id);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE reference_queries (
|
|
50
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
51
|
+
org_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE,
|
|
52
|
+
layer_id uuid NOT NULL,
|
|
53
|
+
|
|
54
|
+
-- Bounded because every one of these is an embedding round trip at the end
|
|
55
|
+
-- of a migration, and because an unbounded text column reached by an API is
|
|
56
|
+
-- a way to store something else.
|
|
57
|
+
query text NOT NULL CHECK (length(query) BETWEEN 1 AND 1024),
|
|
58
|
+
|
|
59
|
+
-- At most ten, which is the `k` the check retrieves at. A set naming more
|
|
60
|
+
-- expected documents than the check can return could never score 1.0, so
|
|
61
|
+
-- the floor would be unreachable and the operator would read it as a
|
|
62
|
+
-- regression. Refused here as well as at the API, because the two disagree
|
|
63
|
+
-- exactly when someone writes to the database by hand.
|
|
64
|
+
expected text[] NOT NULL CHECK (
|
|
65
|
+
cardinality(expected) BETWEEN 1 AND 10
|
|
66
|
+
AND array_position(expected, NULL) IS NULL
|
|
67
|
+
),
|
|
68
|
+
|
|
69
|
+
-- The operator's order, preserved. A reference set is read as a list and a
|
|
70
|
+
-- report that renumbers it every time it is fetched is a report nobody can
|
|
71
|
+
-- compare against the last one.
|
|
72
|
+
ordinal int NOT NULL,
|
|
73
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
74
|
+
|
|
75
|
+
CONSTRAINT reference_queries_layer_in_org
|
|
76
|
+
FOREIGN KEY (layer_id, org_id) REFERENCES layers (id, org_id) ON DELETE CASCADE,
|
|
77
|
+
CONSTRAINT reference_queries_ordinal UNIQUE (layer_id, ordinal)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- The two questions asked of this table: "the set for this layer" (the API, and
|
|
81
|
+
-- the check) and "does this layer have one at all" (the switch predicate).
|
|
82
|
+
CREATE INDEX reference_queries_layer ON reference_queries (org_id, layer_id);
|
|
83
|
+
|
|
84
|
+
-- Tenant data, so RLS, and FORCE because ENABLE alone does not apply to the
|
|
85
|
+
-- owner and migrations run as the owner.
|
|
86
|
+
ALTER TABLE reference_queries ENABLE ROW LEVEL SECURITY;
|
|
87
|
+
ALTER TABLE reference_queries FORCE ROW LEVEL SECURITY;
|
|
88
|
+
|
|
89
|
+
CREATE POLICY org_isolation ON reference_queries
|
|
90
|
+
USING (org_id = current_setting('app.current_org')::uuid);
|
|
91
|
+
|
|
92
|
+
-- `nacre_app` reads and rewrites the set, because the API does. `nacre_worker`
|
|
93
|
+
-- only reads: the check must not be able to alter the thing it is checked
|
|
94
|
+
-- against, and a sweep that could delete a reference query could turn a failing
|
|
95
|
+
-- gate into a passing one by emptying it.
|
|
96
|
+
GRANT SELECT, INSERT, DELETE ON reference_queries TO nacre_app;
|
|
97
|
+
GRANT SELECT ON reference_queries TO nacre_worker;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- 0016 — remove the ACL tag cache.
|
|
2
|
+
--
|
|
3
|
+
-- `docs/authz.md` 3.6 specified `acl_tags` in the vector payload as a second
|
|
4
|
+
-- filter alongside the layer bound: "both the layer filter and the tag filter
|
|
5
|
+
-- apply". The filter was never built. `buildFilter` is the only filter builder
|
|
6
|
+
-- in the codebase and it emits `org_id`, `deleted`, the plan's layers and
|
|
7
|
+
-- documents as a `should`, a `must_not` on denied documents, and the caller's
|
|
8
|
+
-- metadata narrowing — and no tag clause. So the whole subsystem kept a payload
|
|
9
|
+
-- field fresh that no query read.
|
|
10
|
+
--
|
|
11
|
+
-- Two things settled it, and only the second is decisive.
|
|
12
|
+
--
|
|
13
|
+
-- **It saves nothing.** The tags were justified as a speed-up over "joining
|
|
14
|
+
-- back to Postgres". Nothing joins back: the resolver computes the caller's
|
|
15
|
+
-- whole permitted set from `grants` on every request, so the expensive part has
|
|
16
|
+
-- already happened by the time a filter is built. A tag clause would be a
|
|
17
|
+
-- second constraint derived from the same table, one request later.
|
|
18
|
+
--
|
|
19
|
+
-- **It cannot express what the product now sells.** `tagsForLayer` read
|
|
20
|
+
-- `effect = 'allow'` grants on the layer and its workspace — document-scoped
|
|
21
|
+
-- grants were invisible to it, and so were denies. Applied as the `must` the
|
|
22
|
+
-- specification asks for, a caller reaching a document through a
|
|
23
|
+
-- document-scoped grant would have been filtered *out*: their principal is not
|
|
24
|
+
-- in that layer's tag set. Document-scoped grants are issuable now, through
|
|
25
|
+
-- `@nacre.work/enterprise-acl-advanced`, so the specified design and a shipped
|
|
26
|
+
-- feature could not both be true.
|
|
27
|
+
--
|
|
28
|
+
-- Making tags per-document would have fixed that and left the other cost: the
|
|
29
|
+
-- tag filter is stale by construction, so a *newly granted* document would stay
|
|
30
|
+
-- invisible until the sweep caught up. Revocation is already immediate — the
|
|
31
|
+
-- plan is computed per request, and the effective-principals cache is keyed on
|
|
32
|
+
-- `organizations.groups_version`, which bumps on every write to `grants` — so
|
|
33
|
+
-- the trade was a delay on grants in exchange for nothing on revocations.
|
|
34
|
+
--
|
|
35
|
+
-- What goes with it: the retag sweep, its lease, the propagation gauge and its
|
|
36
|
+
-- alert, `NACRE_ACL_TAG_HASH_BYTES` and `NACRE_ACL_PROPAGATION_SLA`. Invariant
|
|
37
|
+
-- I4 is unchanged and is now argued structurally rather than measured
|
|
38
|
+
-- temporally: there is no cache between a grant change and the next request.
|
|
39
|
+
--
|
|
40
|
+
-- `organizations.groups_version` stays. It is the effective-principals cache
|
|
41
|
+
-- key, which is read on every request, and its triggers are what make that
|
|
42
|
+
-- cache safe.
|
|
43
|
+
|
|
44
|
+
ALTER TABLE documents DROP COLUMN IF EXISTS acl_version;
|
|
45
|
+
ALTER TABLE documents DROP COLUMN IF EXISTS acl_tagged_at;
|
|
46
|
+
|
|
47
|
+
-- Dropped with its column. Named explicitly because a partial index over a
|
|
48
|
+
-- dropped column disappears silently, and a reader of 0004 should be able to
|
|
49
|
+
-- find where it went.
|
|
50
|
+
DROP INDEX IF EXISTS documents_acl_version_idx;
|
|
51
|
+
|
|
52
|
+
-- `sweep_claimed_at` **stays**, and 0011's name for it is now misleading rather
|
|
53
|
+
-- than wrong. It was added for the retag sweep and the collector took it as
|
|
54
|
+
-- well: `claimPurgeable` sets it to lease a tombstone so two workers do not
|
|
55
|
+
-- purge the same points, and `markPurged` clears it. Dropping it here removed a
|
|
56
|
+
-- column the collector reads — caught by the garbage-collection tests against a
|
|
57
|
+
-- real database, which is the only place it could have been caught.
|
|
58
|
+
COMMENT ON COLUMN documents.sweep_claimed_at IS
|
|
59
|
+
'Lease held by the garbage collector while it purges a tombstone''s vectors. Cleared by markPurged, and reclaimed after NACRE_INDEX_LEASE if the worker holding it dies. Shared with the retag sweep until 0016 removed that sweep.';
|
|
60
|
+
|
|
61
|
+
COMMENT ON COLUMN organizations.groups_version IS
|
|
62
|
+
'Permission epoch. Bumped by triggers on groups, group_members and grants. Read as the effective-principals cache key — which is what makes caching a permission input safe, since a change composes a different key rather than expiring an old one.';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- 0017 — `users.role` loses `workspace_admin`.
|
|
2
|
+
--
|
|
3
|
+
-- 0001 wrote `CHECK (role IN ('platform_admin','org_admin','workspace_admin','member'))`
|
|
4
|
+
-- and `OrgRole` in the code has always been three values. A row carrying the
|
|
5
|
+
-- fourth read into a type that does not admit it and then behaved as `member`,
|
|
6
|
+
-- because it matches neither role branch in `resolve` and falls through to
|
|
7
|
+
-- grant-based evaluation. Safe — it denies rather than widens — and still a
|
|
8
|
+
-- role the schema offered, nothing implemented, and nothing refused: an
|
|
9
|
+
-- operator who set it got a silently demoted user and no error.
|
|
10
|
+
--
|
|
11
|
+
-- Removed rather than implemented, because it is a category error and not a
|
|
12
|
+
-- missing feature. `users.role` is organization-wide; "administers a
|
|
13
|
+
-- workspace" is scoped to one. The permission model already expresses it, and
|
|
14
|
+
-- expresses it better: `admin` on a `workspace` scope, which a person can hold
|
|
15
|
+
-- on several workspaces at once and which a single column cannot say. Adding
|
|
16
|
+
-- the role would have created a second way to spell one thing, with the
|
|
17
|
+
-- resolver having to decide which wins.
|
|
18
|
+
--
|
|
19
|
+
-- Existing rows are moved to `member`, which is what they already behaved as.
|
|
20
|
+
-- The UPDATE runs before the constraint is replaced, or the constraint would
|
|
21
|
+
-- refuse the rows it exists to remove.
|
|
22
|
+
|
|
23
|
+
UPDATE users SET role = 'member' WHERE role = 'workspace_admin';
|
|
24
|
+
|
|
25
|
+
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check;
|
|
26
|
+
ALTER TABLE users ADD CONSTRAINT users_role_check
|
|
27
|
+
CHECK (role IN ('platform_admin', 'org_admin', 'member'));
|
|
28
|
+
|
|
29
|
+
COMMENT ON COLUMN users.role IS
|
|
30
|
+
'Organization-wide role. Anything scoped to a workspace, a layer or a document is a grant, not a role — see docs/authz.md rules 2 and 3.';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 0018 — `group_members` can actually hold a member only once.
|
|
2
|
+
--
|
|
3
|
+
-- 0001 wrote `UNIQUE (group_id, member_user, member_group)` with a CHECK that
|
|
4
|
+
-- exactly one of the two member columns is non-null. **In Postgres a unique
|
|
5
|
+
-- index treats NULLs as distinct by default**, so two rows with the same
|
|
6
|
+
-- `group_id` and `member_user` and a NULL `member_group` do not conflict — and
|
|
7
|
+
-- every row this table holds has one of the two columns NULL, by construction.
|
|
8
|
+
--
|
|
9
|
+
-- The constraint therefore deduplicated nothing at all, in either direction.
|
|
10
|
+
-- `ON CONFLICT DO NOTHING` against it is a no-op, which is exactly how it was
|
|
11
|
+
-- found: a SCIM group sync adding the same member twice produced two rows and
|
|
12
|
+
-- an idempotent re-sync doubled a group's membership every time it ran.
|
|
13
|
+
--
|
|
14
|
+
-- Not a permission bug. `effectivePrincipals` walks the graph into a `Set`, so
|
|
15
|
+
-- a duplicated edge resolves to the same principal set and grants nothing
|
|
16
|
+
-- extra. What it is: a table that grows without bound under a directory sync,
|
|
17
|
+
-- a membership count that is wrong wherever one is shown, and — worse than
|
|
18
|
+
-- either — a uniqueness guarantee the schema appears to make and does not, so
|
|
19
|
+
-- the next writer reaches for `ON CONFLICT` and gets silence.
|
|
20
|
+
--
|
|
21
|
+
-- `NULLS NOT DISTINCT` needs Postgres 15; the Compose image is 17 and
|
|
22
|
+
-- docs/config.md has always named 17. Dropping to a pair of partial unique
|
|
23
|
+
-- indexes would work on older versions and is not worth carrying a version
|
|
24
|
+
-- floor nothing else needs.
|
|
25
|
+
--
|
|
26
|
+
-- Duplicates are collapsed before the constraint is replaced, or the constraint
|
|
27
|
+
-- refuses the rows it exists to prevent. `ctid` is the only thing that
|
|
28
|
+
-- distinguishes them, which is what "duplicate" means here.
|
|
29
|
+
|
|
30
|
+
DELETE FROM group_members a
|
|
31
|
+
USING group_members b
|
|
32
|
+
WHERE a.ctid > b.ctid
|
|
33
|
+
AND a.group_id = b.group_id
|
|
34
|
+
AND a.org_id = b.org_id
|
|
35
|
+
AND a.member_user IS NOT DISTINCT FROM b.member_user
|
|
36
|
+
AND a.member_group IS NOT DISTINCT FROM b.member_group;
|
|
37
|
+
|
|
38
|
+
ALTER TABLE group_members
|
|
39
|
+
DROP CONSTRAINT IF EXISTS group_members_group_id_member_user_member_group_key;
|
|
40
|
+
|
|
41
|
+
ALTER TABLE group_members
|
|
42
|
+
ADD CONSTRAINT group_members_member_unique
|
|
43
|
+
UNIQUE NULLS NOT DISTINCT (group_id, member_user, member_group);
|
|
44
|
+
|
|
45
|
+
COMMENT ON CONSTRAINT group_members_member_unique ON group_members IS
|
|
46
|
+
'NULLS NOT DISTINCT, deliberately: exactly one of member_user and member_group is non-null on every row, so the default NULLS DISTINCT made this constraint match nothing.';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- 0019 — a tenant may read the installation-wide embedding provider, never write it.
|
|
2
|
+
--
|
|
3
|
+
-- `embedding_providers` is the one table where a NULL `org_id` is legitimate: it
|
|
4
|
+
-- is the installation-wide default model, and 0002's `org_isolation` lets every
|
|
5
|
+
-- organization *read* it, on purpose. 0002 also carried this, two lines down:
|
|
6
|
+
--
|
|
7
|
+
-- -- Writing a global default is not something a tenant may do.
|
|
8
|
+
-- CREATE POLICY org_writes_own ON embedding_providers
|
|
9
|
+
-- FOR INSERT WITH CHECK (org_id = current_setting('app.current_org')::uuid);
|
|
10
|
+
--
|
|
11
|
+
-- and that policy does nothing. A PERMISSIVE policy is OR'd with the other
|
|
12
|
+
-- PERMISSIVE policy for the same command, so on INSERT the effective check is
|
|
13
|
+
-- `org_writes_own` OR `org_isolation` — and `org_isolation`'s check admits
|
|
14
|
+
-- `org_id IS NULL`. `org_writes_own` is a strict subset of what was already
|
|
15
|
+
-- allowed, so adding it changed nothing. It was verified inert against a real
|
|
16
|
+
-- database: a connection scoped to org A, as `nacre_app`, could INSERT, UPDATE
|
|
17
|
+
-- and DELETE the NULL-`org_id` row every other tenant reads as its default.
|
|
18
|
+
--
|
|
19
|
+
-- The consequence is the shape the whole schema's FORCE RLS exists to prevent,
|
|
20
|
+
-- one table over from where it was checked: repoint that endpoint and every
|
|
21
|
+
-- other tenant's documents are embedded by an attacker's service; delete it and
|
|
22
|
+
-- every tenant relying on the default stops resolving a model. No core endpoint
|
|
23
|
+
-- writes this table, so this is the second line of defence rather than a
|
|
24
|
+
-- reachable exploit today — but the second line is exactly what a NULL-`org_id`
|
|
25
|
+
-- row needs, because the first line (a handler setting `org_id` from the token)
|
|
26
|
+
-- cannot cover a row whose `org_id` is meant to be NULL.
|
|
27
|
+
--
|
|
28
|
+
-- The fix is RESTRICTIVE policies on the three writing commands. A RESTRICTIVE
|
|
29
|
+
-- policy is AND'd with the permissive result, so it can only ever remove access
|
|
30
|
+
-- — it cannot widen a read. SELECT carries none, so the global row stays
|
|
31
|
+
-- readable by every tenant exactly as before.
|
|
32
|
+
|
|
33
|
+
DROP POLICY IF EXISTS org_writes_own ON embedding_providers;
|
|
34
|
+
|
|
35
|
+
-- Each names the writing role's own organization and nothing else. `org_id`
|
|
36
|
+
-- NULL fails the equality (NULL = uuid is NULL, not true), which is what keeps
|
|
37
|
+
-- the global row out of a tenant's reach on every one of the three commands.
|
|
38
|
+
CREATE POLICY tenant_writes_own_insert ON embedding_providers
|
|
39
|
+
AS RESTRICTIVE FOR INSERT
|
|
40
|
+
WITH CHECK (org_id = current_setting('app.current_org')::uuid);
|
|
41
|
+
|
|
42
|
+
CREATE POLICY tenant_writes_own_update ON embedding_providers
|
|
43
|
+
AS RESTRICTIVE FOR UPDATE
|
|
44
|
+
USING (org_id = current_setting('app.current_org')::uuid)
|
|
45
|
+
WITH CHECK (org_id = current_setting('app.current_org')::uuid);
|
|
46
|
+
|
|
47
|
+
CREATE POLICY tenant_writes_own_delete ON embedding_providers
|
|
48
|
+
AS RESTRICTIVE FOR DELETE
|
|
49
|
+
USING (org_id = current_setting('app.current_org')::uuid);
|
|
50
|
+
|
|
51
|
+
-- The installation-wide default is still written once, by `init`, and it is a
|
|
52
|
+
-- NULL-`org_id` row — so it now goes in under `nacre_worker`, the same
|
|
53
|
+
-- BYPASSRLS role the worker's cross-tenant queue uses. `init` is the only
|
|
54
|
+
-- writer of that row (a per-organization provider carries the org's own id and
|
|
55
|
+
-- passes the policies above unchanged), which is why the exception is one
|
|
56
|
+
-- statement in one bootstrap command rather than a hole in the policy.
|
|
57
|
+
COMMENT ON POLICY tenant_writes_own_insert ON embedding_providers IS
|
|
58
|
+
'A tenant writes only its own providers. The NULL-org_id installation default is written by init under nacre_worker; see 0019.';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `multipart/form-data`, parsed strictly.
|
|
3
|
+
*
|
|
4
|
+
* `POST /v1/documents` has declared this in `docs/openapi.yaml` since before
|
|
5
|
+
* there was a server, and `docs/api.md` has listed it as not built for just as
|
|
6
|
+
* long. This is it.
|
|
7
|
+
*
|
|
8
|
+
* ─── why by hand, and what that obliges ───
|
|
9
|
+
*
|
|
10
|
+
* This parses attacker-supplied bytes. The usual answer is a library, and the
|
|
11
|
+
* usual argument against one applies here more than anywhere else in this
|
|
12
|
+
* repository: the request path of a document index is exactly where a
|
|
13
|
+
* dependency's parser bugs become yours. The parser sidecar makes the same
|
|
14
|
+
* choice for the same reason and says so.
|
|
15
|
+
*
|
|
16
|
+
* The obligation that comes with it is to be strict rather than clever. Every
|
|
17
|
+
* bound below is a refusal, not a truncation, and none of them is
|
|
18
|
+
* configurable — a caller who trips one gets `400` and a reason:
|
|
19
|
+
*
|
|
20
|
+
* - the boundary must match RFC 2046's grammar, so it cannot carry a regular
|
|
21
|
+
* expression, a newline, or a thousand characters;
|
|
22
|
+
* - a bounded number of parts, so a body of ten thousand empty ones is a
|
|
23
|
+
* refusal rather than ten thousand allocations;
|
|
24
|
+
* - a bounded header block per part, so a part that is all headers cannot be
|
|
25
|
+
* scanned forever;
|
|
26
|
+
* - `filename` is read and never used to build anything. Object keys come from
|
|
27
|
+
* `documentKey`, which hashes the external id; a filename reaches a database
|
|
28
|
+
* column and nothing else.
|
|
29
|
+
*
|
|
30
|
+
* What it deliberately does not do: nested multipart, `Content-Transfer-
|
|
31
|
+
* Encoding`, and charsets other than UTF-8. Each is a real part of the
|
|
32
|
+
* specification and none is something a document upload needs, so each is a
|
|
33
|
+
* refusal rather than a branch.
|
|
34
|
+
*/
|
|
35
|
+
export declare class MultipartError extends Error {
|
|
36
|
+
constructor(message: string);
|
|
37
|
+
}
|
|
38
|
+
export interface MultipartPart {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/** Present when the part carried one. Never used to build a path or a key. */
|
|
41
|
+
readonly filename?: string;
|
|
42
|
+
readonly contentType?: string;
|
|
43
|
+
readonly bytes: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
/** Enough for a document plus its fields, and small enough to bound the work. */
|
|
46
|
+
export declare const MAX_PARTS = 16;
|
|
47
|
+
/** A part's headers. Anything longer is not a header block, it is an attack. */
|
|
48
|
+
export declare const MAX_PART_HEADER_BYTES: number;
|
|
49
|
+
/**
|
|
50
|
+
* The boundary from a `content-type`, or `undefined` when this is not
|
|
51
|
+
* multipart at all — which is not an error, it is how a caller says "JSON".
|
|
52
|
+
*/
|
|
53
|
+
export declare function multipartBoundary(contentType: string | undefined): string | undefined;
|
|
54
|
+
export declare function parseMultipart(body: Uint8Array, boundary: string): readonly MultipartPart[];
|
|
55
|
+
//# sourceMappingURL=multipart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAC3B;AAED,iFAAiF;AACjF,eAAO,MAAM,SAAS,KAAK,CAAA;AAC3B,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,QAAW,CAAA;AAK7C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAqBrF;AAaD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,aAAa,EAAE,CA+E3F"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `multipart/form-data`, parsed strictly.
|
|
3
|
+
*
|
|
4
|
+
* `POST /v1/documents` has declared this in `docs/openapi.yaml` since before
|
|
5
|
+
* there was a server, and `docs/api.md` has listed it as not built for just as
|
|
6
|
+
* long. This is it.
|
|
7
|
+
*
|
|
8
|
+
* ─── why by hand, and what that obliges ───
|
|
9
|
+
*
|
|
10
|
+
* This parses attacker-supplied bytes. The usual answer is a library, and the
|
|
11
|
+
* usual argument against one applies here more than anywhere else in this
|
|
12
|
+
* repository: the request path of a document index is exactly where a
|
|
13
|
+
* dependency's parser bugs become yours. The parser sidecar makes the same
|
|
14
|
+
* choice for the same reason and says so.
|
|
15
|
+
*
|
|
16
|
+
* The obligation that comes with it is to be strict rather than clever. Every
|
|
17
|
+
* bound below is a refusal, not a truncation, and none of them is
|
|
18
|
+
* configurable — a caller who trips one gets `400` and a reason:
|
|
19
|
+
*
|
|
20
|
+
* - the boundary must match RFC 2046's grammar, so it cannot carry a regular
|
|
21
|
+
* expression, a newline, or a thousand characters;
|
|
22
|
+
* - a bounded number of parts, so a body of ten thousand empty ones is a
|
|
23
|
+
* refusal rather than ten thousand allocations;
|
|
24
|
+
* - a bounded header block per part, so a part that is all headers cannot be
|
|
25
|
+
* scanned forever;
|
|
26
|
+
* - `filename` is read and never used to build anything. Object keys come from
|
|
27
|
+
* `documentKey`, which hashes the external id; a filename reaches a database
|
|
28
|
+
* column and nothing else.
|
|
29
|
+
*
|
|
30
|
+
* What it deliberately does not do: nested multipart, `Content-Transfer-
|
|
31
|
+
* Encoding`, and charsets other than UTF-8. Each is a real part of the
|
|
32
|
+
* specification and none is something a document upload needs, so each is a
|
|
33
|
+
* refusal rather than a branch.
|
|
34
|
+
*/
|
|
35
|
+
export class MultipartError extends Error {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'MultipartError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Enough for a document plus its fields, and small enough to bound the work. */
|
|
42
|
+
export const MAX_PARTS = 16;
|
|
43
|
+
/** A part's headers. Anything longer is not a header block, it is an attack. */
|
|
44
|
+
export const MAX_PART_HEADER_BYTES = 8 * 1024;
|
|
45
|
+
/** RFC 2046: 1-70 characters from a fixed set, not ending in a space. */
|
|
46
|
+
const BOUNDARY = /^[A-Za-z0-9'()+_,\-./:=?]([A-Za-z0-9'()+_,\-./:=? ]{0,68}[A-Za-z0-9'()+_,\-./:=?])?$/;
|
|
47
|
+
/**
|
|
48
|
+
* The boundary from a `content-type`, or `undefined` when this is not
|
|
49
|
+
* multipart at all — which is not an error, it is how a caller says "JSON".
|
|
50
|
+
*/
|
|
51
|
+
export function multipartBoundary(contentType) {
|
|
52
|
+
if (contentType === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const [type, ...rest] = contentType.split(';');
|
|
55
|
+
if ((type ?? '').trim().toLowerCase() !== 'multipart/form-data')
|
|
56
|
+
return undefined;
|
|
57
|
+
for (const parameter of rest) {
|
|
58
|
+
const eq = parameter.indexOf('=');
|
|
59
|
+
if (eq === -1)
|
|
60
|
+
continue;
|
|
61
|
+
if (parameter.slice(0, eq).trim().toLowerCase() !== 'boundary')
|
|
62
|
+
continue;
|
|
63
|
+
let value = parameter.slice(eq + 1).trim();
|
|
64
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
65
|
+
value = value.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
if (!BOUNDARY.test(value)) {
|
|
68
|
+
throw new MultipartError('the multipart boundary is not a valid one');
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
throw new MultipartError('multipart/form-data with no boundary parameter');
|
|
73
|
+
}
|
|
74
|
+
const CRLF = Buffer.from('\r\n');
|
|
75
|
+
const DOUBLE_CRLF = Buffer.from('\r\n\r\n');
|
|
76
|
+
/** `name="x"; filename="y"` — one parameter, unescaped the way browsers send it. */
|
|
77
|
+
function parameter(header, key) {
|
|
78
|
+
const match = new RegExp(`(?:^|;)\\s*${key}\\s*=\\s*("([^"]*)"|[^;]*)`, 'i').exec(header);
|
|
79
|
+
if (match === null)
|
|
80
|
+
return undefined;
|
|
81
|
+
const value = match[2] ?? match[1] ?? '';
|
|
82
|
+
return value.trim();
|
|
83
|
+
}
|
|
84
|
+
export function parseMultipart(body, boundary) {
|
|
85
|
+
const buffer = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
86
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
87
|
+
let cursor = buffer.indexOf(delimiter);
|
|
88
|
+
if (cursor === -1)
|
|
89
|
+
throw new MultipartError('the body contains no multipart boundary');
|
|
90
|
+
const parts = [];
|
|
91
|
+
while (cursor !== -1) {
|
|
92
|
+
let start = cursor + delimiter.length;
|
|
93
|
+
// `--boundary--` closes the body. Anything after it is epilogue and
|
|
94
|
+
// ignored, which is what the specification says and what every client does.
|
|
95
|
+
if (buffer.slice(start, start + 2).toString() === '--')
|
|
96
|
+
break;
|
|
97
|
+
// The delimiter is followed by CRLF. Tolerating a bare LF here would mean
|
|
98
|
+
// accepting a body no conforming client sends, so it does not.
|
|
99
|
+
if (!buffer.slice(start, start + 2).equals(CRLF)) {
|
|
100
|
+
throw new MultipartError('a multipart boundary is not followed by CRLF');
|
|
101
|
+
}
|
|
102
|
+
start += 2;
|
|
103
|
+
const headerEnd = buffer.indexOf(DOUBLE_CRLF, start);
|
|
104
|
+
if (headerEnd === -1)
|
|
105
|
+
throw new MultipartError('a multipart part has no header block');
|
|
106
|
+
if (headerEnd - start > MAX_PART_HEADER_BYTES) {
|
|
107
|
+
throw new MultipartError('a multipart part has an implausibly large header block');
|
|
108
|
+
}
|
|
109
|
+
const headers = buffer.slice(start, headerEnd).toString('utf8');
|
|
110
|
+
const bodyStart = headerEnd + DOUBLE_CRLF.length;
|
|
111
|
+
const next = buffer.indexOf(delimiter, bodyStart);
|
|
112
|
+
if (next === -1)
|
|
113
|
+
throw new MultipartError('a multipart part is not terminated');
|
|
114
|
+
// The CRLF immediately before the next delimiter belongs to the delimiter,
|
|
115
|
+
// not to the content. Keeping it appends two bytes to every uploaded file.
|
|
116
|
+
const end = next >= 2 && buffer.slice(next - 2, next).equals(CRLF) ? next - 2 : next;
|
|
117
|
+
let disposition;
|
|
118
|
+
let contentType;
|
|
119
|
+
for (const line of headers.split('\r\n')) {
|
|
120
|
+
const colon = line.indexOf(':');
|
|
121
|
+
if (colon === -1)
|
|
122
|
+
continue;
|
|
123
|
+
const key = line.slice(0, colon).trim().toLowerCase();
|
|
124
|
+
const value = line.slice(colon + 1).trim();
|
|
125
|
+
if (key === 'content-disposition')
|
|
126
|
+
disposition = value;
|
|
127
|
+
else if (key === 'content-type')
|
|
128
|
+
contentType = value;
|
|
129
|
+
else if (key === 'content-transfer-encoding' && value.toLowerCase() !== 'binary') {
|
|
130
|
+
// Refused rather than decoded. base64 here is legal and archaic, and a
|
|
131
|
+
// decoder is another parser of hostile input for a case no browser and
|
|
132
|
+
// no HTTP client produces.
|
|
133
|
+
throw new MultipartError(`content-transfer-encoding ${value} is not supported`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (disposition === undefined)
|
|
137
|
+
throw new MultipartError('a multipart part has no content-disposition');
|
|
138
|
+
const name = parameter(disposition, 'name');
|
|
139
|
+
if (name === undefined || name === '') {
|
|
140
|
+
throw new MultipartError('a multipart part has no name');
|
|
141
|
+
}
|
|
142
|
+
const filename = parameter(disposition, 'filename');
|
|
143
|
+
parts.push({
|
|
144
|
+
name,
|
|
145
|
+
...(filename === undefined || filename === '' ? {} : { filename }),
|
|
146
|
+
...(contentType === undefined ? {} : { contentType }),
|
|
147
|
+
bytes: new Uint8Array(buffer.subarray(bodyStart, end)),
|
|
148
|
+
});
|
|
149
|
+
if (parts.length > MAX_PARTS) {
|
|
150
|
+
throw new MultipartError(`a multipart body may carry at most ${MAX_PARTS} parts`);
|
|
151
|
+
}
|
|
152
|
+
cursor = next;
|
|
153
|
+
}
|
|
154
|
+
if (parts.length === 0)
|
|
155
|
+
throw new MultipartError('the multipart body carries no parts');
|
|
156
|
+
return parts;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=multipart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.js","sourceRoot":"","sources":["../multipart.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -36,6 +36,16 @@ export interface ProtectedResourceMetadata {
|
|
|
36
36
|
}
|
|
37
37
|
/** The path RFC 9728 fixes, and the one the `WWW-Authenticate` header names. */
|
|
38
38
|
export declare const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
39
|
+
/**
|
|
40
|
+
* Where the public signing key is published — RFC 7517, and the conventional
|
|
41
|
+
* path every JWT library looks at first.
|
|
42
|
+
*
|
|
43
|
+
* Served only by a deployment that signs asymmetrically. With
|
|
44
|
+
* `NACRE_JWT_SECRET` there is nothing to publish and this answers `404`: a
|
|
45
|
+
* shared secret has no public half, and an endpoint that produced one anyway
|
|
46
|
+
* would be handing out the key that mints tokens.
|
|
47
|
+
*/
|
|
48
|
+
export declare const JWKS_PATH = "/.well-known/jwks.json";
|
|
39
49
|
export declare function protectedResourceMetadata(input: {
|
|
40
50
|
/** `NACRE_CANONICAL_URL`. Baked into the issuer of every token ever minted. */
|
|
41
51
|
readonly canonicalUrl: string;
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,2BAA2B,CAAA;AAEjD,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
CHANGED
|
@@ -29,6 +29,16 @@
|
|
|
29
29
|
*/
|
|
30
30
|
/** The path RFC 9728 fixes, and the one the `WWW-Authenticate` header names. */
|
|
31
31
|
export const PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource';
|
|
32
|
+
/**
|
|
33
|
+
* Where the public signing key is published — RFC 7517, and the conventional
|
|
34
|
+
* path every JWT library looks at first.
|
|
35
|
+
*
|
|
36
|
+
* Served only by a deployment that signs asymmetrically. With
|
|
37
|
+
* `NACRE_JWT_SECRET` there is nothing to publish and this answers `404`: a
|
|
38
|
+
* shared secret has no public half, and an endpoint that produced one anyway
|
|
39
|
+
* would be handing out the key that mints tokens.
|
|
40
|
+
*/
|
|
41
|
+
export const JWKS_PATH = '/.well-known/jwks.json';
|
|
32
42
|
export function protectedResourceMetadata(input) {
|
|
33
43
|
return {
|
|
34
44
|
// Without a trailing slash, because this is the audience value a token is
|
package/dist/oauth.js.map
CHANGED
|
@@ -1 +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
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAUH,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,uCAAuC,CAAA;AAE9E;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,wBAAwB,CAAA;AAEjD,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;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,
|
|
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,CAyItG"}
|