@opengeni/documents 0.5.18 → 0.5.38

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.
@@ -0,0 +1,14 @@
1
+ import { type KnowledgeRecord, type KnowledgeSource } from "@opengeni/contracts";
2
+ export type KnowledgeProjectionInput = {
3
+ title: string;
4
+ body: string | null;
5
+ summary: string | null;
6
+ topics: unknown;
7
+ metadata: unknown;
8
+ source: KnowledgeSource;
9
+ };
10
+ export type KnowledgeProjectionResult = Pick<KnowledgeRecord, "title" | "content" | "projection"> & {
11
+ source: KnowledgeSource;
12
+ };
13
+ /** Deterministic, byte-bounded projection at the agent-facing Knowledge envelope. */
14
+ export declare function projectKnowledgeRecord(input: KnowledgeProjectionInput): KnowledgeProjectionResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/documents",
3
- "version": "0.5.18",
3
+ "version": "0.5.38",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,10 @@
23
23
  "./google-drive": {
24
24
  "types": "./dist/google-drive.d.ts",
25
25
  "import": "./dist/google-drive.js"
26
+ },
27
+ "./atlassian": {
28
+ "types": "./dist/atlassian.d.ts",
29
+ "import": "./dist/atlassian.js"
26
30
  }
27
31
  },
28
32
  "publishConfig": {
@@ -36,11 +40,11 @@
36
40
  },
37
41
  "dependencies": {
38
42
  "@llamaindex/liteparse": "^1.5.3",
39
- "@opengeni/config": "^0.12.1",
40
- "@opengeni/contracts": "^0.40.0",
41
- "@opengeni/db": "^0.28.9",
42
- "@opengeni/storage": "^0.2.75",
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
47
  "drizzle-orm": "^0.45.2",
44
- "openai": "6.36.0"
48
+ "openai": "6.47.0"
45
49
  }
46
50
  }
@@ -0,0 +1,250 @@
1
+ import type { ScopedKnowledgeScope } from "@opengeni/contracts";
2
+ import {
3
+ connectorDestinationDocumentAuthority,
4
+ resolveConnectorDocumentDestination,
5
+ type ConnectorDocumentDestination,
6
+ } from "@opengeni/contracts/connector-destinations";
7
+ import type { AtlassianSelectedSource } from "@opengeni/contracts/atlassian";
8
+
9
+ export const ATLASSIAN_PROVIDER_KEY = "atlassian" as const;
10
+
11
+ export type AtlassianKnowledgeSourceIdentity = {
12
+ providerKey: typeof ATLASSIAN_PROVIDER_KEY;
13
+ externalTenantId: string;
14
+ externalSourceId: string;
15
+ sourceKind: "atlassian-jira-project" | "atlassian-confluence-space";
16
+ sourceUri: string;
17
+ scope: ScopedKnowledgeScope;
18
+ };
19
+
20
+ export type AtlassianInventoryProviderItem = {
21
+ id: string;
22
+ key: string;
23
+ title: string;
24
+ version: string | null;
25
+ createdAt: string | null;
26
+ updatedAt: string | null;
27
+ webUrl: string;
28
+ };
29
+
30
+ export type AtlassianInventoryPage = {
31
+ items: AtlassianInventoryProviderItem[];
32
+ nextCursor: string | null;
33
+ };
34
+
35
+ export type AtlassianInventoryEntry = {
36
+ externalObjectId: string;
37
+ externalVersionId: string | null;
38
+ sourceId: string;
39
+ parentFolderId: string;
40
+ driveId: null;
41
+ title: string;
42
+ mimeType: "text/markdown";
43
+ modifiedTime: string | null;
44
+ createdTime: string | null;
45
+ sourceUri: string;
46
+ transfer: {
47
+ action: "download";
48
+ contentType: "text/markdown";
49
+ filename: string;
50
+ declaredBytes: null;
51
+ };
52
+ };
53
+
54
+ export type AtlassianInventoryStopReason =
55
+ | "api_request_limit"
56
+ | "elapsed_time_limit"
57
+ | "item_limit"
58
+ | "provider_error";
59
+
60
+ export type AtlassianInventoryCheckpoint = {
61
+ version: 1;
62
+ cloudId: string;
63
+ sourceId: string;
64
+ cursor: string | null;
65
+ itemCount: number;
66
+ apiRequestCount: number;
67
+ };
68
+
69
+ export function atlassianKnowledgeScope(
70
+ destination: ConnectorDocumentDestination,
71
+ ): ScopedKnowledgeScope {
72
+ const authority = connectorDestinationDocumentAuthority(destination);
73
+ if (authority.authorityKind === "organization") {
74
+ return { kind: "organization", workspaceId: null, subjectId: null };
75
+ }
76
+ if (authority.authorityKind === "workspace") {
77
+ if (!authority.authorityWorkspaceId) throw new Error("workspace authority is missing");
78
+ return { kind: "workspace", workspaceId: authority.authorityWorkspaceId, subjectId: null };
79
+ }
80
+ if (!authority.authorityWorkspaceId || !authority.authoritySubjectId) {
81
+ throw new Error("personal authority is missing");
82
+ }
83
+ return {
84
+ kind: "personal",
85
+ workspaceId: authority.authorityWorkspaceId,
86
+ subjectId: authority.authoritySubjectId,
87
+ };
88
+ }
89
+
90
+ export function atlassianKnowledgeSourceIdentity(input: {
91
+ source: AtlassianSelectedSource;
92
+ accountId: string;
93
+ workspaceId: string;
94
+ connectionSubjectId: string;
95
+ }): AtlassianKnowledgeSourceIdentity {
96
+ const destination = resolveConnectorDocumentDestination(input.source.destination, {
97
+ accountId: input.accountId,
98
+ workspaceId: input.workspaceId,
99
+ connectionSubjectId: input.connectionSubjectId,
100
+ });
101
+ const siteUrl = new URL(input.source.siteUrl);
102
+ const sourceUri =
103
+ input.source.kind === "jira_project"
104
+ ? new URL(`/jira/software/c/projects/${encodeURIComponent(input.source.key)}`, siteUrl)
105
+ : new URL(`/wiki/spaces/${encodeURIComponent(input.source.key)}`, siteUrl);
106
+ return {
107
+ providerKey: ATLASSIAN_PROVIDER_KEY,
108
+ externalTenantId: bounded(input.source.cloudId, 256, "cloudId"),
109
+ externalSourceId: bounded(input.source.id, 300, "source.id"),
110
+ sourceKind:
111
+ input.source.kind === "jira_project"
112
+ ? "atlassian-jira-project"
113
+ : "atlassian-confluence-space",
114
+ sourceUri: sourceUri.toString(),
115
+ scope: atlassianKnowledgeScope(destination),
116
+ };
117
+ }
118
+
119
+ export async function inventoryAtlassianSource(input: {
120
+ cloudId: string;
121
+ source: AtlassianSelectedSource;
122
+ limits: { maxItems: number; maxApiRequests: number; maxElapsedMs: number; pageSize: number };
123
+ checkpoint: Record<string, unknown> | null;
124
+ listPage: (cursor: string | null, pageSize: number) => Promise<AtlassianInventoryPage>;
125
+ }): Promise<{
126
+ status: "complete" | "paused";
127
+ stopReason: AtlassianInventoryStopReason | null;
128
+ entries: AtlassianInventoryEntry[];
129
+ checkpoint: AtlassianInventoryCheckpoint | null;
130
+ providerRequests: number;
131
+ elapsedMs: number;
132
+ }> {
133
+ const startedAt = Date.now();
134
+ const restored = parseCheckpoint(input.checkpoint, input.cloudId, input.source.id);
135
+ let cursor = restored?.cursor ?? null;
136
+ let itemCount = restored?.itemCount ?? 0;
137
+ let apiRequestCount = restored?.apiRequestCount ?? 0;
138
+ const entries: AtlassianInventoryEntry[] = [];
139
+
140
+ while (true) {
141
+ if (Date.now() - startedAt >= input.limits.maxElapsedMs) return paused("elapsed_time_limit");
142
+ if (apiRequestCount >= input.limits.maxApiRequests) return paused("api_request_limit");
143
+ if (itemCount >= input.limits.maxItems) return paused("item_limit");
144
+
145
+ let page: AtlassianInventoryPage;
146
+ try {
147
+ page = await input.listPage(
148
+ cursor,
149
+ Math.min(input.limits.pageSize, input.limits.maxItems - itemCount),
150
+ );
151
+ } catch {
152
+ return paused("provider_error");
153
+ }
154
+ apiRequestCount += 1;
155
+ for (const item of page.items) {
156
+ if (itemCount >= input.limits.maxItems) return paused("item_limit");
157
+ entries.push(atlassianEntry(input.source, item));
158
+ itemCount += 1;
159
+ }
160
+ cursor = page.nextCursor;
161
+ if (!cursor) {
162
+ return {
163
+ status: "complete",
164
+ stopReason: null,
165
+ entries,
166
+ checkpoint: null,
167
+ providerRequests: apiRequestCount - (restored?.apiRequestCount ?? 0),
168
+ elapsedMs: Date.now() - startedAt,
169
+ };
170
+ }
171
+ }
172
+
173
+ function paused(stopReason: AtlassianInventoryStopReason) {
174
+ return {
175
+ status: "paused" as const,
176
+ stopReason,
177
+ entries,
178
+ checkpoint: {
179
+ version: 1 as const,
180
+ cloudId: input.cloudId,
181
+ sourceId: input.source.id,
182
+ cursor,
183
+ itemCount,
184
+ apiRequestCount,
185
+ },
186
+ providerRequests: apiRequestCount - (restored?.apiRequestCount ?? 0),
187
+ elapsedMs: Date.now() - startedAt,
188
+ };
189
+ }
190
+ }
191
+
192
+ function atlassianEntry(
193
+ source: AtlassianSelectedSource,
194
+ item: AtlassianInventoryProviderItem,
195
+ ): AtlassianInventoryEntry {
196
+ const title = bounded(item.title, 1024, "title");
197
+ return {
198
+ externalObjectId: bounded(`${source.kind}:${item.id}`, 512, "item.id"),
199
+ externalVersionId: item.version,
200
+ sourceId: source.id,
201
+ parentFolderId: source.resourceId,
202
+ driveId: null,
203
+ title,
204
+ mimeType: "text/markdown",
205
+ modifiedTime: item.updatedAt,
206
+ createdTime: item.createdAt,
207
+ sourceUri: new URL(item.webUrl).toString(),
208
+ transfer: {
209
+ action: "download",
210
+ contentType: "text/markdown",
211
+ filename: `${safeFilename(item.key || title)}.md`,
212
+ declaredBytes: null,
213
+ },
214
+ };
215
+ }
216
+
217
+ function parseCheckpoint(
218
+ value: Record<string, unknown> | null,
219
+ cloudId: string,
220
+ sourceId: string,
221
+ ): AtlassianInventoryCheckpoint | null {
222
+ if (!value) return null;
223
+ if (
224
+ value.version !== 1 ||
225
+ value.cloudId !== cloudId ||
226
+ value.sourceId !== sourceId ||
227
+ (value.cursor !== null && typeof value.cursor !== "string") ||
228
+ !Number.isSafeInteger(value.itemCount) ||
229
+ !Number.isSafeInteger(value.apiRequestCount)
230
+ ) {
231
+ throw new Error("invalid Atlassian inventory checkpoint");
232
+ }
233
+ return value as AtlassianInventoryCheckpoint;
234
+ }
235
+
236
+ function bounded(value: string, max: number, label: string): string {
237
+ const normalized = value.trim();
238
+ if (!normalized || normalized.length > max) throw new Error(`invalid ${label}`);
239
+ return normalized;
240
+ }
241
+
242
+ function safeFilename(value: string): string {
243
+ return (
244
+ value
245
+ .normalize("NFKC")
246
+ .replace(/[\\/:*?"<>|\0-\x1f]/g, "_")
247
+ .trim()
248
+ .slice(0, 220) || "item"
249
+ );
250
+ }
@@ -262,6 +262,7 @@ export function googleDriveKnowledgeScope(
262
262
 
263
263
  export function googleDriveKnowledgeSourceIdentity(input: {
264
264
  googlePermissionId: string;
265
+ googleEmail?: string | undefined;
265
266
  source: Pick<GoogleDriveSelectedSource, "id" | "driveId" | "destination" | "targetScope">;
266
267
  accountId?: string | undefined;
267
268
  workspaceId: string;
@@ -270,7 +271,11 @@ export function googleDriveKnowledgeSourceIdentity(input: {
270
271
  initiatingSubjectId?: string | undefined;
271
272
  }): GoogleDriveKnowledgeSourceIdentity {
272
273
  const sourceId = driveId(input.source.id, "source.id");
273
- const externalTenantId = normalizedGooglePermissionId(input.googlePermissionId);
274
+ // permissionId identifies a principal, not a Google tenant. Consumer
275
+ // accounts intentionally share one provider tenant partition; Workspace
276
+ // accounts use their normalized hosted domain. Per-principal authority stays
277
+ // on the immutable connection owner binding.
278
+ const externalTenantId = googleTenantIdentity(input.googleEmail);
274
279
  const sourceDriveId = nullableDriveId(input.source.driveId, "source.driveId");
275
280
  const destination = resolveConnectorDocumentDestination(input.source.destination, {
276
281
  accountId: input.accountId ?? input.workspaceId,
@@ -295,6 +300,19 @@ export function googleDriveKnowledgeSourceIdentity(input: {
295
300
  };
296
301
  }
297
302
 
303
+ function googleTenantIdentity(email: string | undefined): string {
304
+ if (email === undefined) return "google-consumer";
305
+ const normalized = email.trim().toLowerCase();
306
+ const separator = normalized.lastIndexOf("@");
307
+ if (separator <= 0 || separator === normalized.length - 1) {
308
+ throw new Error("googleEmail must be a normalized email address");
309
+ }
310
+ const domain = normalized.slice(separator + 1);
311
+ return domain === "gmail.com" || domain === "googlemail.com"
312
+ ? "google-consumer"
313
+ : `google-workspace:${domain}`;
314
+ }
315
+
298
316
  export function planGoogleDriveTransfer(
299
317
  item: GoogleDriveInventoryProviderItem,
300
318
  maxFileBytes: number,
@@ -340,6 +358,7 @@ export function planGoogleDriveTransfer(
340
358
 
341
359
  export async function inventoryGoogleDriveSource(input: {
342
360
  googlePermissionId: string;
361
+ googleEmail?: string | undefined;
343
362
  source: GoogleDriveSelectedSource;
344
363
  accountId?: string | undefined;
345
364
  workspaceId: string;
@@ -355,6 +374,7 @@ export async function inventoryGoogleDriveSource(input: {
355
374
  const googlePermissionId = normalizedGooglePermissionId(input.googlePermissionId);
356
375
  const source = googleDriveKnowledgeSourceIdentity({
357
376
  googlePermissionId,
377
+ googleEmail: input.googleEmail,
358
378
  source: input.source,
359
379
  ...(input.accountId ? { accountId: input.accountId } : {}),
360
380
  workspaceId: input.workspaceId,