@opengeni/documents 0.5.38 → 0.5.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/documents",
3
- "version": "0.5.38",
3
+ "version": "0.5.41",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,10 +40,10 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@llamaindex/liteparse": "^1.5.3",
43
- "@opengeni/config": "^0.16.1",
44
- "@opengeni/contracts": "^0.50.0",
45
- "@opengeni/db": "^0.36.0",
46
- "@opengeni/storage": "^0.2.93",
43
+ "@opengeni/config": "^0.16.4",
44
+ "@opengeni/contracts": "^1.0.1",
45
+ "@opengeni/db": "^1.0.1",
46
+ "@opengeni/storage": "^0.2.96",
47
47
  "drizzle-orm": "^0.45.2",
48
48
  "openai": "6.47.0"
49
49
  }
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import type {
3
- AddDocumentRequest,
2
+ import {
3
+ KnowledgeProviderCitation,
4
+ type KnowledgeProviderCitation as KnowledgeProviderCitationValue,
5
+ type AddDocumentRequest,
4
6
  CreateDocumentBaseRequest,
5
7
  Document,
6
8
  DocumentAuthorityKind,
@@ -20,7 +22,7 @@ import type {
20
22
  ListIndexedDocumentsResponse,
21
23
  } from "@opengeni/contracts";
22
24
  import {
23
- requireFile,
25
+ getFilesForSubject,
24
26
  rlsContextForWorkspace,
25
27
  setSubjectRlsContext,
26
28
  withRlsContext,
@@ -967,7 +969,18 @@ export async function addDocumentToBase(
967
969
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
968
970
  const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
969
971
  if (!base) throw new Error(`Document base not found: ${input.baseId}`);
970
- const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
972
+ const initiatingSubjectId = cleanString(input.initiatingSubjectId ?? null);
973
+ const createdBy = cleanString(input.createdBy ?? null);
974
+ if (initiatingSubjectId && createdBy && initiatingSubjectId !== createdBy) {
975
+ throw new Error("document file authority must match the exact initiating subject");
976
+ }
977
+ const fileAuthoritySubjectId = initiatingSubjectId ?? createdBy ?? null;
978
+ const file = await requireReadyFile(scopedDb, {
979
+ accountId: input.accountId,
980
+ workspaceId: input.workspaceId,
981
+ subjectId: fileAuthoritySubjectId,
982
+ fileId: input.fileId,
983
+ });
971
984
  const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
972
985
  if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
973
986
  throw new Error("knowledge source document identity exceeds 512 characters");
@@ -1054,7 +1067,7 @@ export async function addDocumentToBase(
1054
1067
  authoritySubjectId: authority.subjectId,
1055
1068
  visibility: authority.kind === "personal" ? "private" : "workspace",
1056
1069
  agentAccess: input.agentAccess ?? true,
1057
- createdBy: input.createdBy ?? null,
1070
+ createdBy: fileAuthoritySubjectId,
1058
1071
  curationStatus: input.curationStatus ?? "none",
1059
1072
  updatedAt: now,
1060
1073
  })
@@ -1403,6 +1416,30 @@ export async function getDocument(
1403
1416
  });
1404
1417
  }
1405
1418
 
1419
+ /**
1420
+ * Internal ingestion-only read used after the worker has independently resolved
1421
+ * and fenced the immutable document authority tuple. It deliberately does not
1422
+ * apply provider retrieval authorization because a new Drive document must be
1423
+ * indexed before its first ACL evidence can be attached. User, API, MCP, and
1424
+ * agent reads must use getDocument/effective retrieval instead.
1425
+ */
1426
+ export async function getDocumentForIndexing(
1427
+ db: Database,
1428
+ workspaceId: string,
1429
+ documentId: string,
1430
+ ): Promise<Document | null> {
1431
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1432
+ const [row] = await scopedDb
1433
+ .select()
1434
+ .from(schema.documents)
1435
+ .where(
1436
+ and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId)),
1437
+ )
1438
+ .limit(1);
1439
+ return row ? mapDocument(row) : null;
1440
+ });
1441
+ }
1442
+
1406
1443
  export async function queueDocumentForReindex(
1407
1444
  db: Database,
1408
1445
  workspaceId: string,
@@ -1477,7 +1514,12 @@ export async function indexDocumentNow(
1477
1514
  );
1478
1515
  if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
1479
1516
  let document: DocumentRow = loadedDocument;
1480
- const file = await requireReadyFile(db, workspaceId, document.fileId);
1517
+ const file = await requireReadyFile(db, {
1518
+ accountId: document.accountId,
1519
+ workspaceId,
1520
+ subjectId: cleanString(document.createdBy) ?? null,
1521
+ fileId: document.fileId,
1522
+ });
1481
1523
  await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1482
1524
  await scopedDb
1483
1525
  .update(schema.documents)
@@ -1604,9 +1646,7 @@ export async function indexDocumentNow(
1604
1646
  // Internal indexing must be able to return a private document to the caller
1605
1647
  // that created/queued it. Public reads remain fail-closed when no subject is
1606
1648
  // supplied; the creator subject is the document's frozen access principal.
1607
- const updated = await getDocument(db, workspaceId, documentId, {
1608
- viewerSubjectId: document.authoritySubjectId,
1609
- });
1649
+ const updated = await getDocumentForIndexing(db, workspaceId, documentId);
1610
1650
  if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
1611
1651
  return updated;
1612
1652
  }
@@ -1839,7 +1879,11 @@ export async function searchEffectiveKnowledge(
1839
1879
  access,
1840
1880
  async (scopedDb) =>
1841
1881
  await scopedDb
1842
- .select({ chunk: schema.documentChunks, document: schema.documents })
1882
+ .select({
1883
+ chunk: schema.documentChunks,
1884
+ document: schema.documents,
1885
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1886
+ })
1843
1887
  .from(schema.documentChunks)
1844
1888
  .innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id))
1845
1889
  .where(
@@ -1862,7 +1906,7 @@ export async function searchEffectiveKnowledge(
1862
1906
  if (!row) return [];
1863
1907
  return [
1864
1908
  {
1865
- record: knowledgeChunkRecord(row.document, row.chunk),
1909
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation),
1866
1910
  retrieval: {
1867
1911
  score: rankedResult.score,
1868
1912
  matchType: rankedResult.matchType,
@@ -1895,8 +1939,11 @@ export async function getEffectiveKnowledgeRecord(
1895
1939
  access,
1896
1940
  async (scopedDb) => {
1897
1941
  if (target.kind === "document") {
1898
- const [document] = await scopedDb
1899
- .select()
1942
+ const [row] = await scopedDb
1943
+ .select({
1944
+ document: schema.documents,
1945
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1946
+ })
1900
1947
  .from(schema.documents)
1901
1948
  .where(
1902
1949
  and(
@@ -1907,10 +1954,14 @@ export async function getEffectiveKnowledgeRecord(
1907
1954
  ),
1908
1955
  )
1909
1956
  .limit(1);
1910
- return document ? knowledgeDocumentRecord(document) : null;
1957
+ return row ? knowledgeDocumentRecord(row.document, row.citation) : null;
1911
1958
  }
1912
1959
  const [row] = await scopedDb
1913
- .select({ chunk: schema.documentChunks, document: schema.documents })
1960
+ .select({
1961
+ chunk: schema.documentChunks,
1962
+ document: schema.documents,
1963
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1964
+ })
1914
1965
  .from(schema.documentChunks)
1915
1966
  .innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id))
1916
1967
  .where(
@@ -1923,7 +1974,7 @@ export async function getEffectiveKnowledgeRecord(
1923
1974
  ),
1924
1975
  )
1925
1976
  .limit(1);
1926
- return row ? knowledgeChunkRecord(row.document, row.chunk) : null;
1977
+ return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation) : null;
1927
1978
  },
1928
1979
  );
1929
1980
  }
@@ -1988,7 +2039,11 @@ export async function browseEffectiveKnowledge(
1988
2039
  .limit(1);
1989
2040
  if (!authorizedParent) return { records: [], nextCursor: null, hasMore: false };
1990
2041
  const rows = await scopedDb
1991
- .select({ chunk: schema.documentChunks, document: schema.documents })
2042
+ .select({
2043
+ chunk: schema.documentChunks,
2044
+ document: schema.documents,
2045
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2046
+ })
1992
2047
  .from(schema.documentChunks)
1993
2048
  .innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id))
1994
2049
  .where(
@@ -2006,7 +2061,7 @@ export async function browseEffectiveKnowledge(
2006
2061
  const page = rows.slice(0, limit);
2007
2062
  const last = page.at(-1)?.chunk.chunkIndex;
2008
2063
  return {
2009
- records: page.map((row) => knowledgeChunkRecord(row.document, row.chunk)),
2064
+ records: page.map((row) => knowledgeChunkRecord(row.document, row.chunk, row.citation)),
2010
2065
  nextCursor:
2011
2066
  hasMore && last !== undefined
2012
2067
  ? encodeKnowledgeBrowseCursor(cursorScope, BigInt(last + 1))
@@ -2026,16 +2081,19 @@ export async function browseEffectiveKnowledge(
2026
2081
  if (sourceKinds.length > 0)
2027
2082
  conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
2028
2083
  const rows = await scopedDb
2029
- .select()
2084
+ .select({
2085
+ document: schema.documents,
2086
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2087
+ })
2030
2088
  .from(schema.documents)
2031
2089
  .where(and(...conditions))
2032
2090
  .orderBy(asc(schema.documents.indexSequence))
2033
2091
  .limit(limit + 1);
2034
2092
  const hasMore = rows.length > limit;
2035
2093
  const page = rows.slice(0, limit);
2036
- const last = page.at(-1)?.indexSequence;
2094
+ const last = page.at(-1)?.document.indexSequence;
2037
2095
  return {
2038
- records: page.map(knowledgeDocumentRecord),
2096
+ records: page.map((row) => knowledgeDocumentRecord(row.document, row.citation)),
2039
2097
  nextCursor:
2040
2098
  hasMore && last !== undefined && last !== null
2041
2099
  ? encodeKnowledgeBrowseCursor(cursorScope, last)
@@ -2132,7 +2190,10 @@ function parseKnowledgeRecordId(value: string): {
2132
2190
  return { kind: match[1] as "document" | "document_chunk", id: match[2]!.toLowerCase() };
2133
2191
  }
2134
2192
 
2135
- function knowledgeDocumentRecord(document: typeof schema.documents.$inferSelect): KnowledgeRecord {
2193
+ function knowledgeDocumentRecord(
2194
+ document: typeof schema.documents.$inferSelect,
2195
+ citation: unknown = null,
2196
+ ): KnowledgeRecord {
2136
2197
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
2137
2198
  const projected = projectKnowledgeRecord({
2138
2199
  title: document.title,
@@ -2151,6 +2212,7 @@ function knowledgeDocumentRecord(document: typeof schema.documents.$inferSelect)
2151
2212
  provenance: {
2152
2213
  source: projected.source,
2153
2214
  indexedAt: document.indexedAt.toISOString(),
2215
+ citation: parseKnowledgeProviderCitation(citation),
2154
2216
  },
2155
2217
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
2156
2218
  quality: knowledgeQuality(document),
@@ -2162,6 +2224,7 @@ function knowledgeDocumentRecord(document: typeof schema.documents.$inferSelect)
2162
2224
  function knowledgeChunkRecord(
2163
2225
  document: typeof schema.documents.$inferSelect,
2164
2226
  chunk: typeof schema.documentChunks.$inferSelect,
2227
+ citation: unknown = null,
2165
2228
  ): KnowledgeRecord {
2166
2229
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
2167
2230
  const projected = projectKnowledgeRecord({
@@ -2181,6 +2244,7 @@ function knowledgeChunkRecord(
2181
2244
  provenance: {
2182
2245
  source: projected.source,
2183
2246
  indexedAt: document.indexedAt.toISOString(),
2247
+ citation: parseKnowledgeProviderCitation(citation),
2184
2248
  },
2185
2249
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
2186
2250
  quality: knowledgeQuality(document),
@@ -2259,6 +2323,7 @@ async function vectorSearchDocuments(
2259
2323
  authorityKind: schema.documents.authorityKind,
2260
2324
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
2261
2325
  authoritySubjectId: schema.documents.authoritySubjectId,
2326
+ citation: googleDriveCitationProjection(input.workspaceId, input.access),
2262
2327
  distance,
2263
2328
  })
2264
2329
  .from(schema.documentChunks)
@@ -2309,6 +2374,7 @@ async function keywordSearchDocuments(
2309
2374
  authorityKind: schema.documents.authorityKind,
2310
2375
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
2311
2376
  authoritySubjectId: schema.documents.authoritySubjectId,
2377
+ citation: googleDriveCitationProjection(input.workspaceId, input.access),
2312
2378
  rank,
2313
2379
  })
2314
2380
  .from(schema.documentChunks)
@@ -2365,6 +2431,7 @@ export async function getDocumentChunk(
2365
2431
  authorityKind: schema.documents.authorityKind,
2366
2432
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
2367
2433
  authoritySubjectId: schema.documents.authoritySubjectId,
2434
+ citation: googleDriveCitationProjection(workspaceId, access),
2368
2435
  })
2369
2436
  .from(schema.documentChunks)
2370
2437
  .innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id))
@@ -2492,10 +2559,17 @@ function documentAccessConditions(
2492
2559
  const authority = viewer
2493
2560
  ? (or(organization, workspace, personal) ?? organization)
2494
2561
  : (or(organization, workspace) ?? organization);
2562
+ const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
2563
+ const providerAuthorization = sql`google_drive_file_authorized(
2564
+ ${schema.documents.accountId},
2565
+ ${workspaceId}::uuid,
2566
+ ${viewerSql},
2567
+ ${schema.documents.fileId}
2568
+ )`;
2495
2569
  if (access?.agentOnly) {
2496
- return [eq(schema.documents.agentAccess, true), authority];
2570
+ return [eq(schema.documents.agentAccess, true), authority, providerAuthorization];
2497
2571
  }
2498
- return [authority];
2572
+ return [authority, providerAuthorization];
2499
2573
  }
2500
2574
 
2501
2575
  function documentMatchesAccess(
@@ -2606,6 +2680,7 @@ function mapSearchRowBase(row: {
2606
2680
  authorityKind: string;
2607
2681
  authorityWorkspaceId: string | null;
2608
2682
  authoritySubjectId: string | null;
2683
+ citation?: unknown;
2609
2684
  }): SearchRowBase {
2610
2685
  return {
2611
2686
  chunkId: row.chunkId,
@@ -2629,9 +2704,29 @@ function mapSearchRowBase(row: {
2629
2704
  authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
2630
2705
  authorityWorkspaceId: row.authorityWorkspaceId,
2631
2706
  authoritySubjectId: row.authoritySubjectId,
2707
+ citation: parseKnowledgeProviderCitation(row.citation),
2632
2708
  };
2633
2709
  }
2634
2710
 
2711
+ function googleDriveCitationProjection(
2712
+ workspaceId: string,
2713
+ access: DocumentAccessFilter | undefined,
2714
+ ): SQL<unknown> {
2715
+ const viewer = cleanString(access?.viewerSubjectId ?? null);
2716
+ const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
2717
+ return sql`google_drive_document_citation(
2718
+ ${schema.documents.accountId},
2719
+ ${workspaceId}::uuid,
2720
+ ${viewerSql},
2721
+ ${schema.documents.id},
2722
+ ${schema.documents.fileId}
2723
+ )`;
2724
+ }
2725
+
2726
+ function parseKnowledgeProviderCitation(value: unknown): KnowledgeProviderCitationValue | null {
2727
+ return value === null || value === undefined ? null : KnowledgeProviderCitation.parse(value);
2728
+ }
2729
+
2635
2730
  function mergeDocumentSearchRows(
2636
2731
  rows: CombinedSearchRow[],
2637
2732
  mode: DocumentSearchMode,
@@ -2763,12 +2858,22 @@ export function deterministicEmbedding(
2763
2858
 
2764
2859
  async function requireReadyFile(
2765
2860
  db: Database,
2766
- workspaceId: string,
2767
- fileId: string,
2861
+ input: {
2862
+ accountId: string;
2863
+ workspaceId: string;
2864
+ subjectId: string | null;
2865
+ fileId: string;
2866
+ },
2768
2867
  ): Promise<FileAsset> {
2769
- const file = await requireFile(db, workspaceId, fileId);
2868
+ const [file] = await getFilesForSubject(db, {
2869
+ accountId: input.accountId,
2870
+ workspaceId: input.workspaceId,
2871
+ subjectId: input.subjectId,
2872
+ fileIds: [input.fileId],
2873
+ });
2874
+ if (!file) throw new Error(`File not found: ${input.fileId}`);
2770
2875
  if (file.status !== "ready") {
2771
- throw new Error(`File ${fileId} is ${file.status}`);
2876
+ throw new Error(`File ${input.fileId} is ${file.status}`);
2772
2877
  }
2773
2878
  return file;
2774
2879
  }