@nacre.work/api 0.2.0 → 0.4.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/adapters.d.ts +33 -5
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +235 -30
- package/dist/adapters.js.map +1 -1
- package/dist/auth.d.ts +16 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +26 -0
- package/dist/auth.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +10 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +72 -25
- package/dist/init.js.map +1 -1
- package/dist/login.d.ts +13 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +6 -1
- package/dist/login.js.map +1 -1
- package/dist/main.js +42 -10
- package/dist/main.js.map +1 -1
- package/dist/pagination.d.ts +26 -2
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +11 -2
- package/dist/pagination.js.map +1 -1
- package/dist/rebuild-collection.d.ts +2 -0
- package/dist/rebuild-collection.d.ts.map +1 -0
- package/dist/rebuild-collection.js +169 -0
- package/dist/rebuild-collection.js.map +1 -0
- package/dist/server.d.ts +120 -37
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +413 -11
- package/dist/server.js.map +1 -1
- package/dist/service-keys.d.ts.map +1 -1
- package/dist/service-keys.js +7 -2
- package/dist/service-keys.js.map +1 -1
- package/package.json +2 -2
package/dist/adapters.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildFilter, VectorStore, type CacheStore, type Metadata } from '@nacre.work/core';
|
|
2
2
|
import type { Pool } from 'pg';
|
|
3
3
|
import { type Reranker } from './rerank.js';
|
|
4
|
-
import type { Reindex, ReindexOutcome, ReindexStatus, AuditEvent, AuditQuery, AuditReader, AuditRecord,
|
|
4
|
+
import type { Reindex, ReindexOutcome, ReindexStatus, ReferenceQueries, ReferenceQuery, AuditEvent, AuditQuery, AuditReader, AuditRecord, AuditWriter, Documents, GrantInput, GrantRecord, Grants, Ingest, IngestOutcome, IngestRefused, IngestRequest, Job, Jobs, Layer, LayerOutcome, DocumentView, Layers, Page, PageResult, Workspace, WorkspaceOutcome, Workspaces, SearchHit, SearchOptions, SearchService } from './server.js';
|
|
5
5
|
import type { AuthContext } from './auth.js';
|
|
6
6
|
/**
|
|
7
7
|
* The adapters that put the permission model on the request path.
|
|
@@ -212,7 +212,7 @@ export declare class NacreSearchService implements SearchService {
|
|
|
212
212
|
* by migration 0002, so a bug here cannot rewrite history and neither can
|
|
213
213
|
* anyone holding these credentials.
|
|
214
214
|
*/
|
|
215
|
-
export declare class PostgresAudit implements
|
|
215
|
+
export declare class PostgresAudit implements AuditWriter {
|
|
216
216
|
private readonly pool;
|
|
217
217
|
private readonly role?;
|
|
218
218
|
constructor(pool: Pool, role?: string | undefined);
|
|
@@ -245,7 +245,6 @@ export declare class HttpEmbedder implements Embedder {
|
|
|
245
245
|
static pool(timeoutMs?: number): (provider: EmbeddingProvider) => Embedder;
|
|
246
246
|
embed(texts: readonly string[]): Promise<readonly (readonly number[])[]>;
|
|
247
247
|
}
|
|
248
|
-
export { aclTags };
|
|
249
248
|
/**
|
|
250
249
|
* Queueing a document, with the write permission checked first.
|
|
251
250
|
*
|
|
@@ -289,7 +288,7 @@ export declare class NacreIngest implements Ingest {
|
|
|
289
288
|
private get scope();
|
|
290
289
|
/** The layers this caller may write to, by slug. */
|
|
291
290
|
private writableLayer;
|
|
292
|
-
queue(auth: AuthContext, request: IngestRequest): Promise<IngestOutcome | undefined>;
|
|
291
|
+
queue(auth: AuthContext, request: IngestRequest): Promise<IngestOutcome | IngestRefused | undefined>;
|
|
293
292
|
remove(auth: AuthContext, documentId: string): Promise<boolean>;
|
|
294
293
|
}
|
|
295
294
|
/**
|
|
@@ -598,4 +597,33 @@ export declare class PostgresReindex implements Reindex {
|
|
|
598
597
|
private remaining;
|
|
599
598
|
private statusOf;
|
|
600
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* The reference query set behind the reindex recall gate.
|
|
602
|
+
*
|
|
603
|
+
* `admin` on the layer for both operations, and that is a decision rather than
|
|
604
|
+
* a copy of the reindex adapter above. Reading the set reveals which documents
|
|
605
|
+
* an operator considers the canonical answers to a query, which is a statement
|
|
606
|
+
* about the layer's contents; rule 7 makes `admin` the permission that implies
|
|
607
|
+
* being allowed to see them, and rule 6 means `write` does not.
|
|
608
|
+
*/
|
|
609
|
+
export declare class PostgresReferenceQueries implements ReferenceQueries {
|
|
610
|
+
private readonly pool;
|
|
611
|
+
private readonly role?;
|
|
612
|
+
private readonly principalsCache?;
|
|
613
|
+
constructor(pool: Pool, role?: string | undefined, principalsCache?: PrincipalsCache | undefined);
|
|
614
|
+
private get scope();
|
|
615
|
+
/**
|
|
616
|
+
* The layer, if this caller may administer it.
|
|
617
|
+
*
|
|
618
|
+
* `undefined` for a layer that is not there and for one they may not touch —
|
|
619
|
+
* one answer, which the handler turns into `404`. Deleted layers are not
|
|
620
|
+
* there: a reference set on a deleted layer describes nothing.
|
|
621
|
+
*/
|
|
622
|
+
private administrable;
|
|
623
|
+
list(auth: AuthContext, layerId: string): Promise<readonly ReferenceQuery[] | undefined>;
|
|
624
|
+
replace(auth: AuthContext, layerId: string, queries: readonly {
|
|
625
|
+
query: string;
|
|
626
|
+
expected: readonly string[];
|
|
627
|
+
}[]): Promise<readonly ReferenceQuery[] | undefined>;
|
|
628
|
+
}
|
|
601
629
|
//# sourceMappingURL=adapters.d.ts.map
|
package/dist/adapters.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
|
package/dist/adapters.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildFilter, cachedEffectivePrincipals, documentKey, effectivePrincipals, fromStateJson, loadGrants, loadGroupsVersion, loadScopeTree, PostgresGroupGraph, referenceAllows, reindexProgress, activeResolver, admitIngest, toStateJson, VectorStore, vectorName, withOrg, } from '@nacre.work/core';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { encodeCursor, pageOf } from './pagination.js';
|
|
4
4
|
import { applyRanking } from './rerank.js';
|
|
@@ -53,7 +53,7 @@ export class PostgresDocuments {
|
|
|
53
53
|
// The plan first. Reading the row and then checking is the same
|
|
54
54
|
// information leak with extra steps if the check is ever forgotten,
|
|
55
55
|
// and the layer a document sits in is what the grant is on.
|
|
56
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
56
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
57
57
|
if (plan.kind === 'none')
|
|
58
58
|
return undefined;
|
|
59
59
|
const { rows } = await client.query(`SELECT d.id, d.title, d.layer_id, l.slug AS layer, d.status, d.chunk_count,
|
|
@@ -127,7 +127,7 @@ export class PostgresDocuments {
|
|
|
127
127
|
const collection = orgs[0]?.vector_collection;
|
|
128
128
|
if (collection === undefined)
|
|
129
129
|
return false;
|
|
130
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'write');
|
|
130
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'write');
|
|
131
131
|
if (plan.kind === 'none')
|
|
132
132
|
return false;
|
|
133
133
|
const { rows } = await client.query(`SELECT layer_id FROM documents
|
|
@@ -174,7 +174,7 @@ export class NacreSearchService {
|
|
|
174
174
|
const principals = await principalsFor(client, auth, this.deps.principalsCache);
|
|
175
175
|
const grants = await loadGrants(client, auth.orgId, principals);
|
|
176
176
|
const tree = await loadScopeTree(client, auth.orgId, grants.filter((g) => g.scope.type === 'document').map((g) => g.scope.id));
|
|
177
|
-
const plan = resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'read');
|
|
177
|
+
const plan = activeResolver().resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'read');
|
|
178
178
|
// Every live layer with the model it is on. Small — an installation
|
|
179
179
|
// runs tens of layers per organization, not thousands — and it is what
|
|
180
180
|
// decides how many branches the query has.
|
|
@@ -519,6 +519,10 @@ export class PostgresAudit {
|
|
|
519
519
|
event.requestId,
|
|
520
520
|
]);
|
|
521
521
|
}, this.role === undefined ? {} : { role: this.role });
|
|
522
|
+
// Forwarding to a module's sinks is deliberately *not* here. It is
|
|
523
|
+
// `withAuditSinks`, applied to whatever port a surface was handed, because
|
|
524
|
+
// a sink wants every recorded event and not every event this adapter
|
|
525
|
+
// happened to record.
|
|
522
526
|
}
|
|
523
527
|
}
|
|
524
528
|
/**
|
|
@@ -593,7 +597,6 @@ export class HttpEmbedder {
|
|
|
593
597
|
});
|
|
594
598
|
}
|
|
595
599
|
}
|
|
596
|
-
export { aclTags };
|
|
597
600
|
export class NacreIngest {
|
|
598
601
|
deps;
|
|
599
602
|
constructor(deps) {
|
|
@@ -610,7 +613,7 @@ export class NacreIngest {
|
|
|
610
613
|
const principals = await principalsFor(client, auth, this.deps.principalsCache);
|
|
611
614
|
const grants = await loadGrants(client, auth.orgId, principals);
|
|
612
615
|
const tree = await loadScopeTree(client, auth.orgId, grants.filter((g) => g.scope.type === 'document').map((g) => g.scope.id));
|
|
613
|
-
const plan = resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'write');
|
|
616
|
+
const plan = activeResolver().resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'write');
|
|
614
617
|
if (plan.kind === 'none')
|
|
615
618
|
return undefined;
|
|
616
619
|
const { rows } = await client.query(`SELECT id FROM layers WHERE org_id = $1 AND slug = $2 AND deleted_at IS NULL`, [auth.orgId, layerSlug]);
|
|
@@ -629,8 +632,42 @@ export class NacreIngest {
|
|
|
629
632
|
if (layerId === undefined)
|
|
630
633
|
return undefined;
|
|
631
634
|
const inline = request.content !== undefined;
|
|
632
|
-
const
|
|
633
|
-
|
|
635
|
+
const binary = request.bytes !== undefined;
|
|
636
|
+
if (binary && this.deps.objects === undefined) {
|
|
637
|
+
// The handler refuses this at the edge, naming NACRE_S3_*; reaching
|
|
638
|
+
// here means a second surface started passing bytes without the
|
|
639
|
+
// check. Refusing keeps the invariant local: the bytes' only home is
|
|
640
|
+
// the bucket, and there is no bucket.
|
|
641
|
+
throw new Error('binary ingest requires object storage, and this deployment has none');
|
|
642
|
+
}
|
|
643
|
+
const source = binary ? undefined : inline ? request.content : request.url;
|
|
644
|
+
// For a binary source the hash is over the uploaded bytes — the worker
|
|
645
|
+
// computes the same, so the upsert's idempotency semantics hold across
|
|
646
|
+
// both halves: re-sending the same file is a no-op, a changed file
|
|
647
|
+
// re-indexes. For text it stays over the text, which is the same
|
|
648
|
+
// statement.
|
|
649
|
+
const hash = binary
|
|
650
|
+
? createHash('sha256').update(request.bytes).digest('hex')
|
|
651
|
+
: createHash('sha256').update(source, 'utf8').digest('hex');
|
|
652
|
+
// A module's ingest gate, after write is established and before anything
|
|
653
|
+
// is stored, so a refusal leaves neither a row nor an object behind. No
|
|
654
|
+
// gate registered is the open core admitting every document a caller may
|
|
655
|
+
// write, which is what it did before this point existed. A refusal
|
|
656
|
+
// becomes the handler's 4xx; it is not `undefined`, because that would
|
|
657
|
+
// read as "unwritable" and hide the layer the caller just wrote against.
|
|
658
|
+
const refusal = await admitIngest({
|
|
659
|
+
orgId: auth.orgId,
|
|
660
|
+
layerId,
|
|
661
|
+
principal: auth.principal,
|
|
662
|
+
role: auth.role,
|
|
663
|
+
externalId: request.externalId,
|
|
664
|
+
bytes: binary
|
|
665
|
+
? request.bytes.byteLength
|
|
666
|
+
: new TextEncoder().encode(source).byteLength,
|
|
667
|
+
});
|
|
668
|
+
if (refusal !== undefined) {
|
|
669
|
+
return { refused: true, status: refusal.status, reason: refusal.reason };
|
|
670
|
+
}
|
|
634
671
|
// Bytes to object storage before the row, never after.
|
|
635
672
|
//
|
|
636
673
|
// The reverse fails unrecoverably, which is the same argument the
|
|
@@ -643,17 +680,25 @@ export class NacreIngest {
|
|
|
643
680
|
// copying somebody else's URL into our bucket at ingest time would be
|
|
644
681
|
// a fetch on the request path — which is exactly what the worker does
|
|
645
682
|
// later, with a timeout and a sandbox.
|
|
646
|
-
let sourceType = inline ? 'inline' : 'url';
|
|
683
|
+
let sourceType = binary ? 's3' : inline ? 'inline' : 'url';
|
|
647
684
|
let sourceRef = source;
|
|
648
|
-
if (
|
|
685
|
+
if (binary) {
|
|
686
|
+
// A binary source has exactly one home. The bytes go up with their
|
|
687
|
+
// real Content-Type, so a presigned GET later answers with the type
|
|
688
|
+
// the caller sent rather than a lie a browser would act on.
|
|
689
|
+
const key = documentKey(auth.orgId, layerId, request.externalId);
|
|
690
|
+
await this.deps.objects.put(key, request.bytes, request.contentType);
|
|
691
|
+
sourceRef = key;
|
|
692
|
+
}
|
|
693
|
+
else if (inline && this.deps.objects !== undefined) {
|
|
649
694
|
const key = documentKey(auth.orgId, layerId, request.externalId);
|
|
650
695
|
await this.deps.objects.put(key, new TextEncoder().encode(source), 'text/plain');
|
|
651
696
|
sourceType = 's3';
|
|
652
697
|
sourceRef = key;
|
|
653
698
|
}
|
|
654
699
|
const { rows } = await client.query(`INSERT INTO documents
|
|
655
|
-
(org_id, layer_id, external_id, source_type, source_ref, title, content_hash, metadata, status)
|
|
656
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'pending')
|
|
700
|
+
(org_id, layer_id, external_id, source_type, source_ref, title, content_hash, metadata, content_type, status)
|
|
701
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'pending')
|
|
657
702
|
ON CONFLICT (layer_id, external_id) DO UPDATE SET
|
|
658
703
|
-- Only touch the row when the content actually changed. A repeat
|
|
659
704
|
-- with the same bytes must not re-queue work: every client that
|
|
@@ -663,6 +708,10 @@ export class NacreIngest {
|
|
|
663
708
|
title = COALESCE(EXCLUDED.title, documents.title),
|
|
664
709
|
content_hash = EXCLUDED.content_hash,
|
|
665
710
|
metadata = EXCLUDED.metadata,
|
|
711
|
+
-- The worker dispatches on this. A document re-sent as a different
|
|
712
|
+
-- format carries a different hash too, so the status CASE below
|
|
713
|
+
-- already re-queues it; this just keeps the row telling the truth.
|
|
714
|
+
content_type = EXCLUDED.content_type,
|
|
666
715
|
-- Metadata counts as a change, because it is written into the
|
|
667
716
|
-- vector payload of every point and a filter reads it from there.
|
|
668
717
|
-- Updating the row alone would leave the two disagreeing: the
|
|
@@ -687,6 +736,7 @@ export class NacreIngest {
|
|
|
687
736
|
request.title ?? null,
|
|
688
737
|
`sha256:${hash}`,
|
|
689
738
|
JSON.stringify(request.metadata),
|
|
739
|
+
binary ? request.contentType : 'text/plain',
|
|
690
740
|
]);
|
|
691
741
|
const row = rows[0];
|
|
692
742
|
if (row === undefined)
|
|
@@ -713,7 +763,7 @@ export class NacreIngest {
|
|
|
713
763
|
const principals = await principalsFor(client, auth, this.deps.principalsCache);
|
|
714
764
|
const grants = await loadGrants(client, auth.orgId, principals);
|
|
715
765
|
const tree = await loadScopeTree(client, auth.orgId, [documentId]);
|
|
716
|
-
const plan = resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'write');
|
|
766
|
+
const plan = activeResolver().resolve({ orgId: auth.orgId, role: auth.role, principals, grants, tree }, 'write');
|
|
717
767
|
if (plan.kind === 'none')
|
|
718
768
|
return false;
|
|
719
769
|
const { rows } = await client.query(`SELECT layer_id FROM documents
|
|
@@ -786,7 +836,7 @@ export class PostgresJobs {
|
|
|
786
836
|
// different projection — and needs the same check. The status and the
|
|
787
837
|
// error string are both facts about a document the caller may not be
|
|
788
838
|
// allowed to know exists.
|
|
789
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
839
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
790
840
|
if (plan.kind === 'none')
|
|
791
841
|
return undefined;
|
|
792
842
|
const { rows } = await client.query(`SELECT id, status, error, chunk_count, layer_id FROM documents
|
|
@@ -879,7 +929,7 @@ export class PostgresLayers {
|
|
|
879
929
|
*/
|
|
880
930
|
async list(auth, page) {
|
|
881
931
|
return withOrg(this.pool, auth.orgId, async (client) => {
|
|
882
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
932
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
883
933
|
if (plan.kind === 'none')
|
|
884
934
|
return { items: [], nextCursor: null };
|
|
885
935
|
// The count comes from the same statement. It is what a catalog is
|
|
@@ -893,7 +943,12 @@ export class PostgresLayers {
|
|
|
893
943
|
// grants exist — a commercial module — this becomes a number that
|
|
894
944
|
// discloses more than the caller can reach, and it has to be recomputed
|
|
895
945
|
// per caller or dropped.
|
|
946
|
+
// `created_at::text` beside `created_at`, and the cursor is built from
|
|
947
|
+
// it. See `Position.createdAt` — a timestamp that has been through a
|
|
948
|
+
// JavaScript `Date` is truncated to milliseconds, and a truncated bound
|
|
949
|
+
// matches the row it came from all over again.
|
|
896
950
|
const projection = `l.id, l.slug, l.name, l.workspace_id, l.description, l.created_at,
|
|
951
|
+
l.created_at::text AS created_at_text,
|
|
897
952
|
(SELECT count(*) FROM documents d
|
|
898
953
|
WHERE d.layer_id = l.id AND d.deleted_at IS NULL) AS document_count`;
|
|
899
954
|
// Ordered by (created_at, id) rather than by slug, because that is the
|
|
@@ -921,7 +976,10 @@ export class PostgresLayers {
|
|
|
921
976
|
documentCount: Number(r.document_count),
|
|
922
977
|
createdAt: r.created_at.toISOString(),
|
|
923
978
|
}));
|
|
924
|
-
return pageOf(layers, page, (l) => ({
|
|
979
|
+
return pageOf(layers, page, (l, i) => ({
|
|
980
|
+
createdAt: rows[i].created_at_text,
|
|
981
|
+
id: l.id,
|
|
982
|
+
}));
|
|
925
983
|
}, this.scope);
|
|
926
984
|
}
|
|
927
985
|
async update(auth, layerId, input) {
|
|
@@ -1101,13 +1159,28 @@ export class PostgresWorkspaces {
|
|
|
1101
1159
|
// administer, and therefore unable to create the first layer in it,
|
|
1102
1160
|
// which is the deadlock this endpoint was added to break. It was
|
|
1103
1161
|
// written that way first and caught by running it.
|
|
1104
|
-
const plan = resolve(context, 'read');
|
|
1105
|
-
|
|
1162
|
+
const plan = activeResolver().resolve(context, 'read');
|
|
1163
|
+
// Seek and limit in SQL.
|
|
1164
|
+
//
|
|
1165
|
+
// Neither was here: the statement fetched every workspace in the
|
|
1166
|
+
// organization, ignored `page.after` entirely, and handed the whole
|
|
1167
|
+
// list to `pageOf` — which then answered "there is another page"
|
|
1168
|
+
// because the list was at least as long as the limit. So
|
|
1169
|
+
// `GET /v1/workspaces` returned the same complete list on every page
|
|
1170
|
+
// and never terminated, and a client following `next_cursor` looped
|
|
1171
|
+
// forever. Found by a test that walked the listing one item at a time.
|
|
1172
|
+
const after = page?.after;
|
|
1173
|
+
const seek = after === undefined ? '' : ' AND (w.created_at, w.id) > ($2::timestamptz, $3::uuid)';
|
|
1174
|
+
// One extra, because the filter below removes rows the caller may not
|
|
1175
|
+
// reach: without it a page whose last row is filtered out would report
|
|
1176
|
+
// itself as the end of the collection.
|
|
1177
|
+
const cap = page === undefined ? '' : ` LIMIT ${page.limit + 1}`;
|
|
1178
|
+
const { rows } = await client.query(`SELECT w.id, w.slug, w.name, w.created_at, w.created_at::text AS created_at_text,
|
|
1106
1179
|
(SELECT array_agg(l.id) FROM layers l
|
|
1107
1180
|
WHERE l.workspace_id = w.id AND l.deleted_at IS NULL) AS layer_ids
|
|
1108
1181
|
FROM workspaces w
|
|
1109
|
-
WHERE w.org_id = $1 AND w.deleted_at IS NULL
|
|
1110
|
-
ORDER BY w.created_at, w.id`, [auth.orgId]);
|
|
1182
|
+
WHERE w.org_id = $1 AND w.deleted_at IS NULL${seek}
|
|
1183
|
+
ORDER BY w.created_at, w.id${cap}`, after === undefined ? [auth.orgId] : [auth.orgId, after.createdAt, after.id]);
|
|
1111
1184
|
const reachable = rows.filter((w) => {
|
|
1112
1185
|
if (plan.kind === 'all')
|
|
1113
1186
|
return true;
|
|
@@ -1120,14 +1193,28 @@ export class PostgresWorkspaces {
|
|
|
1120
1193
|
// for a workspace with nothing in it yet.
|
|
1121
1194
|
return referenceAllows(context, { type: 'workspace', id: w.id }, 'read');
|
|
1122
1195
|
});
|
|
1123
|
-
const
|
|
1196
|
+
const fetched = page === undefined ? rows : rows.slice(0, page.limit);
|
|
1197
|
+
const items = reachable
|
|
1198
|
+
.filter((w) => fetched.some((f) => f.id === w.id))
|
|
1199
|
+
.map((w) => ({
|
|
1124
1200
|
id: w.id,
|
|
1125
1201
|
slug: w.slug,
|
|
1126
1202
|
name: w.name,
|
|
1127
1203
|
layerCount: (w.layer_ids ?? []).length,
|
|
1128
1204
|
createdAt: w.created_at.toISOString(),
|
|
1129
1205
|
}));
|
|
1130
|
-
|
|
1206
|
+
// From the last row **fetched**, not the last returned — the same rule
|
|
1207
|
+
// the grant listing follows and for the same reason: the filter above
|
|
1208
|
+
// removes rows the caller may not reach, and a cursor taken from a
|
|
1209
|
+
// survivor would skip everything the filter dropped after it.
|
|
1210
|
+
//
|
|
1211
|
+
// Which makes a short page normal here rather than a signal, and
|
|
1212
|
+
// `next_cursor` the only thing that says whether more exist.
|
|
1213
|
+
const lastFetched = fetched[fetched.length - 1];
|
|
1214
|
+
const nextCursor = page !== undefined && rows.length > page.limit && lastFetched !== undefined
|
|
1215
|
+
? encodeCursor({ createdAt: lastFetched.created_at_text, id: lastFetched.id })
|
|
1216
|
+
: null;
|
|
1217
|
+
return { items, nextCursor };
|
|
1131
1218
|
}, this.scope);
|
|
1132
1219
|
}
|
|
1133
1220
|
/**
|
|
@@ -1250,14 +1337,14 @@ export class PostgresGrants {
|
|
|
1250
1337
|
async list(auth, page) {
|
|
1251
1338
|
return withOrg(this.pool, auth.orgId, async (client) => {
|
|
1252
1339
|
const context = await contextFor(client, auth, this.principalsCache);
|
|
1253
|
-
const plan = resolve(context, 'admin');
|
|
1340
|
+
const plan = activeResolver().resolve(context, 'admin');
|
|
1254
1341
|
if (plan.kind === 'none')
|
|
1255
1342
|
return { items: [], nextCursor: null };
|
|
1256
1343
|
const after = page?.after;
|
|
1257
1344
|
const seek = after === undefined ? '' : ' AND (created_at, id) > ($2::timestamptz, $3::uuid)';
|
|
1258
1345
|
const cap = page === undefined ? '' : ` LIMIT ${page.limit}`;
|
|
1259
1346
|
const { rows } = await client.query(`SELECT id, principal_type, principal_id, scope_type, scope_id, permission, effect, source,
|
|
1260
|
-
created_at
|
|
1347
|
+
created_at, created_at::text AS created_at_text
|
|
1261
1348
|
FROM grants WHERE org_id = $1${seek} ORDER BY created_at, id${cap}`, after === undefined ? [auth.orgId] : [auth.orgId, after.createdAt, after.id]);
|
|
1262
1349
|
const visible = rows
|
|
1263
1350
|
.filter((r) => referenceAllows(context, { type: r.scope_type, id: r.scope_id }, 'admin'))
|
|
@@ -1277,7 +1364,7 @@ export class PostgresGrants {
|
|
|
1277
1364
|
// it — a page of grants silently missing from an administrator's view.
|
|
1278
1365
|
const lastFetched = rows[rows.length - 1];
|
|
1279
1366
|
const nextCursor = page !== undefined && rows.length >= page.limit && lastFetched !== undefined
|
|
1280
|
-
? encodeCursor({ createdAt: lastFetched.
|
|
1367
|
+
? encodeCursor({ createdAt: lastFetched.created_at_text, id: lastFetched.id })
|
|
1281
1368
|
: null;
|
|
1282
1369
|
return { items: visible, nextCursor };
|
|
1283
1370
|
}, this.scope);
|
|
@@ -1293,6 +1380,33 @@ export class PostgresGrants {
|
|
|
1293
1380
|
if (!referenceAllows(context, { type: input.scopeType, id: input.scopeId }, 'admin')) {
|
|
1294
1381
|
return undefined;
|
|
1295
1382
|
}
|
|
1383
|
+
// And the scope has to exist in this organization, which the check
|
|
1384
|
+
// above does not establish. `referenceAllows` returns `true` for
|
|
1385
|
+
// `org_admin` on its third line — rule 3, admin on every scope
|
|
1386
|
+
// implicitly — and it returns *before* the scope is placed, so an
|
|
1387
|
+
// administrator naming any uuid at all passed.
|
|
1388
|
+
//
|
|
1389
|
+
// Not a leak, and the reason is worth stating rather than assumed: a
|
|
1390
|
+
// grant on a scope that is not here becomes `should: layer_id = X`
|
|
1391
|
+
// beside an unconditional `must: org_id = <this tenant>`, and points
|
|
1392
|
+
// carrying layer X belong to another tenant and live in another
|
|
1393
|
+
// collection. The second line of defence holds. What got written was a
|
|
1394
|
+
// row that permits nothing and points at nothing.
|
|
1395
|
+
//
|
|
1396
|
+
// It still has to be refused. An administrator reading `/v1/grants`
|
|
1397
|
+
// sees a grant they cannot explain on an id they cannot look up, and
|
|
1398
|
+
// `404` stops meaning what invariant 4 says it means — "no permission"
|
|
1399
|
+
// and "no such object" are one answer, which requires that naming a
|
|
1400
|
+
// nonexistent object *reaches* that answer rather than succeeding.
|
|
1401
|
+
//
|
|
1402
|
+
// Written out per table rather than assembled, so the table name can
|
|
1403
|
+
// never come from a caller-supplied string. `scopeType` is a checked
|
|
1404
|
+
// union already; this makes it structural.
|
|
1405
|
+
const exists = await client.query(input.scopeType === 'workspace'
|
|
1406
|
+
? `SELECT 1 FROM workspaces WHERE org_id = $1 AND id = $2 AND deleted_at IS NULL`
|
|
1407
|
+
: `SELECT 1 FROM layers WHERE org_id = $1 AND id = $2 AND deleted_at IS NULL`, [auth.orgId, input.scopeId]);
|
|
1408
|
+
if (exists.rowCount !== 1)
|
|
1409
|
+
return undefined;
|
|
1296
1410
|
const { rows } = await client.query(`INSERT INTO grants
|
|
1297
1411
|
(org_id, principal_type, principal_id, scope_type, scope_id, permission, effect, source)
|
|
1298
1412
|
VALUES ($1,$2,$3,$4,$5,$6,'allow','api')
|
|
@@ -1446,7 +1560,8 @@ export class PostgresAuditReader {
|
|
|
1446
1560
|
// without a second query and without a count — a count over a
|
|
1447
1561
|
// retention window is the expensive thing this endpoint must not do on
|
|
1448
1562
|
// every request.
|
|
1449
|
-
const { rows } = await client.query(`SELECT id::text, occurred_at,
|
|
1563
|
+
const { rows } = await client.query(`SELECT id::text, occurred_at, occurred_at::text AS occurred_at_text,
|
|
1564
|
+
actor_type, actor_id, actor_label, action,
|
|
1450
1565
|
surface, client, target, result, detail, request_id
|
|
1451
1566
|
FROM audit_events
|
|
1452
1567
|
WHERE ${where.join(' AND ')}
|
|
@@ -1466,9 +1581,19 @@ export class PostgresAuditReader {
|
|
|
1466
1581
|
detail: r.detail ?? {},
|
|
1467
1582
|
requestId: r.request_id,
|
|
1468
1583
|
}));
|
|
1584
|
+
// From the row rather than from the mapped record, because the record
|
|
1585
|
+
// carries an ISO string and this cursor needs full precision.
|
|
1586
|
+
//
|
|
1587
|
+
// This one **skips** rather than repeats, and that is the same bug seen
|
|
1588
|
+
// from the other side: the ordering is descending, so a bound truncated
|
|
1589
|
+
// downwards excludes the row it came from *and* everything between the
|
|
1590
|
+
// truncated value and the real one. On a listing that would lose a page
|
|
1591
|
+
// of layers; on the access log it silently loses events, in the one
|
|
1592
|
+
// place the product promises a precise answer.
|
|
1593
|
+
const lastRow = rows[Math.min(page.limit, rows.length) - 1];
|
|
1469
1594
|
const last = items[items.length - 1];
|
|
1470
|
-
const nextCursor = rows.length > page.limit && last !== undefined
|
|
1471
|
-
? encodeCursor({ createdAt:
|
|
1595
|
+
const nextCursor = rows.length > page.limit && last !== undefined && lastRow !== undefined
|
|
1596
|
+
? encodeCursor({ createdAt: lastRow.occurred_at_text, id: last.id })
|
|
1472
1597
|
: null;
|
|
1473
1598
|
return { items, nextCursor };
|
|
1474
1599
|
}, this.scopeFor());
|
|
@@ -1520,7 +1645,7 @@ export class PostgresReindex {
|
|
|
1520
1645
|
// vector in the layer and changes which model answers every future
|
|
1521
1646
|
// query — `read` is nowhere near enough, and `write` is about putting
|
|
1522
1647
|
// documents in rather than about the layer itself.
|
|
1523
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'admin');
|
|
1648
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'admin');
|
|
1524
1649
|
if (plan.kind === 'none')
|
|
1525
1650
|
return undefined;
|
|
1526
1651
|
// Locked for the whole decision. Two starts arriving together would
|
|
@@ -1635,7 +1760,7 @@ export class PostgresReindex {
|
|
|
1635
1760
|
// being migrated is not an administrative fact — it explains why a
|
|
1636
1761
|
// result set moved — and hiding it from someone who can already read
|
|
1637
1762
|
// the layer buys nothing.
|
|
1638
|
-
const plan = resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
1763
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'read');
|
|
1639
1764
|
if (plan.kind === 'none')
|
|
1640
1765
|
return undefined;
|
|
1641
1766
|
const { rows } = await client.query(`SELECT id, vector_name, reindex_state FROM layers
|
|
@@ -1686,7 +1811,87 @@ export class PostgresReindex {
|
|
|
1686
1811
|
failed: state.failed,
|
|
1687
1812
|
progress: reindexProgress(live),
|
|
1688
1813
|
error: state.error ?? null,
|
|
1814
|
+
check: state.check ?? null,
|
|
1689
1815
|
};
|
|
1690
1816
|
}
|
|
1691
1817
|
}
|
|
1818
|
+
/**
|
|
1819
|
+
* The reference query set behind the reindex recall gate.
|
|
1820
|
+
*
|
|
1821
|
+
* `admin` on the layer for both operations, and that is a decision rather than
|
|
1822
|
+
* a copy of the reindex adapter above. Reading the set reveals which documents
|
|
1823
|
+
* an operator considers the canonical answers to a query, which is a statement
|
|
1824
|
+
* about the layer's contents; rule 7 makes `admin` the permission that implies
|
|
1825
|
+
* being allowed to see them, and rule 6 means `write` does not.
|
|
1826
|
+
*/
|
|
1827
|
+
export class PostgresReferenceQueries {
|
|
1828
|
+
pool;
|
|
1829
|
+
role;
|
|
1830
|
+
principalsCache;
|
|
1831
|
+
constructor(pool, role, principalsCache) {
|
|
1832
|
+
this.pool = pool;
|
|
1833
|
+
this.role = role;
|
|
1834
|
+
this.principalsCache = principalsCache;
|
|
1835
|
+
}
|
|
1836
|
+
get scope() {
|
|
1837
|
+
return this.role === undefined ? {} : { role: this.role };
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* The layer, if this caller may administer it.
|
|
1841
|
+
*
|
|
1842
|
+
* `undefined` for a layer that is not there and for one they may not touch —
|
|
1843
|
+
* one answer, which the handler turns into `404`. Deleted layers are not
|
|
1844
|
+
* there: a reference set on a deleted layer describes nothing.
|
|
1845
|
+
*/
|
|
1846
|
+
async administrable(client, auth, layerId) {
|
|
1847
|
+
const plan = activeResolver().resolve(await contextFor(client, auth, this.principalsCache), 'admin');
|
|
1848
|
+
if (plan.kind === 'none')
|
|
1849
|
+
return false;
|
|
1850
|
+
if (plan.kind === 'scoped' && !plan.layers.includes(layerId))
|
|
1851
|
+
return false;
|
|
1852
|
+
const { rows } = await client.query('SELECT id FROM layers WHERE org_id = $1 AND id = $2 AND deleted_at IS NULL', [auth.orgId, layerId]);
|
|
1853
|
+
return rows.length > 0;
|
|
1854
|
+
}
|
|
1855
|
+
async list(auth, layerId) {
|
|
1856
|
+
return withOrg(this.pool, auth.orgId, async (client) => {
|
|
1857
|
+
if (!(await this.administrable(client, auth, layerId)))
|
|
1858
|
+
return undefined;
|
|
1859
|
+
const { rows } = await client.query(`SELECT id, query, expected FROM reference_queries
|
|
1860
|
+
WHERE org_id = $1 AND layer_id = $2 ORDER BY ordinal`, [auth.orgId, layerId]);
|
|
1861
|
+
return rows.map((r) => ({ id: r.id, query: r.query, expected: r.expected }));
|
|
1862
|
+
}, this.scope);
|
|
1863
|
+
}
|
|
1864
|
+
async replace(auth, layerId, queries) {
|
|
1865
|
+
return withOrg(this.pool, auth.orgId, async (client) => {
|
|
1866
|
+
if (!(await this.administrable(client, auth, layerId)))
|
|
1867
|
+
return undefined;
|
|
1868
|
+
// Delete then insert, in the transaction `withOrg` already opened. A
|
|
1869
|
+
// set is one statement about the layer, so there is no moment at which
|
|
1870
|
+
// half of the old one and half of the new one are both in the table —
|
|
1871
|
+
// which matters because the worker reads this to decide whether a layer
|
|
1872
|
+
// has a gate at all.
|
|
1873
|
+
await client.query('DELETE FROM reference_queries WHERE org_id = $1 AND layer_id = $2', [
|
|
1874
|
+
auth.orgId,
|
|
1875
|
+
layerId,
|
|
1876
|
+
]);
|
|
1877
|
+
const inserted = [];
|
|
1878
|
+
for (const [ordinal, q] of queries.entries()) {
|
|
1879
|
+
const { rows } = await client.query(`INSERT INTO reference_queries (org_id, layer_id, query, expected, ordinal)
|
|
1880
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING id`, [auth.orgId, layerId, q.query, [...q.expected], ordinal]);
|
|
1881
|
+
inserted.push({
|
|
1882
|
+
id: rows[0]?.id,
|
|
1883
|
+
query: q.query,
|
|
1884
|
+
expected: [...q.expected],
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
// The external ids are deliberately **not** validated against
|
|
1888
|
+
// `documents` here. A reference set is often written before the
|
|
1889
|
+
// documents it names are ingested, and refusing it then would make the
|
|
1890
|
+
// gate impossible to set up on a new layer. The check resolves them
|
|
1891
|
+
// when it runs, and an entry that resolves to nothing fails the reindex
|
|
1892
|
+
// by name — which is the moment it actually matters.
|
|
1893
|
+
return inserted;
|
|
1894
|
+
}, this.scope);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1692
1897
|
//# sourceMappingURL=adapters.js.map
|