fuli-context 0.5.0 → 0.7.2
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/README.en.md +2 -333
- package/README.md +370 -202
- package/README.zh-CN.md +448 -0
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
- package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
- package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
- package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
- package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-CbDsKrsO.js +1 -0
- package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
- package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
- package/dist/web/assets/i18n-BQcns1PH.js +5 -0
- package/dist/web/assets/identity-B8K4VYg6.js +1 -0
- package/dist/web/assets/index-CB8Xmsc2.js +2 -0
- package/dist/web/assets/index-CZeuPh-U.css +1 -0
- package/dist/web/assets/model-eF52GgPF.js +1 -0
- package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
- package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
- package/dist/web/index.html +9 -6
- package/docs/external-knowledge-architecture.md +279 -0
- package/examples/external-knowledge/markdown-folder.mjs +235 -0
- package/graph-provider/fuli_graph/app.py +57 -1
- package/graph-provider/fuli_graph/graph_models.py +3 -0
- package/graph-provider/fuli_graph/graph_query.py +62 -16
- package/graph-provider/fuli_graph/knowledge_review.py +612 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
- package/graph-provider/fuli_graph/knowledge_search.py +51 -4
- package/graph-provider/fuli_graph/models.py +19 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +5 -407
- package/package.json +7 -5
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +73 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +79 -4
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +51 -28
- package/src/cli/local-runtime-options.js +14 -8
- package/src/cli/setup-agent-selection.js +12 -12
- package/src/cli/setup-command.js +41 -18
- package/src/cli/shell-argument.js +10 -0
- package/src/cli/uninstall-command.js +17 -15
- package/src/cli/update-command.js +38 -30
- package/src/external-knowledge/catalog.js +61 -0
- package/src/external-knowledge/conflict-policy.js +58 -0
- package/src/external-knowledge/connected-search.js +166 -0
- package/src/external-knowledge/connectors/custom.js +133 -0
- package/src/external-knowledge/connectors/feishu.js +461 -0
- package/src/external-knowledge/connectors/mcp.js +438 -0
- package/src/external-knowledge/connectors/notion.js +307 -0
- package/src/external-knowledge/connectors/retrieval-api.js +222 -0
- package/src/external-knowledge/document-mapping.js +151 -0
- package/src/external-knowledge/graph-projection.js +17 -0
- package/src/external-knowledge/index.js +8 -0
- package/src/external-knowledge/registry.js +91 -0
- package/src/external-knowledge/runtime.js +68 -0
- package/src/external-knowledge/safe-diagnostic.js +14 -0
- package/src/external-knowledge/service.js +811 -0
- package/src/graphiti/agent-knowledge-workflows.js +7 -2
- package/src/graphiti/federated-application.js +66 -41
- package/src/graphiti/knowledge-item-role.js +21 -0
- package/src/graphiti/knowledge-review.js +43 -0
- package/src/graphiti/project-profile-mapping.js +32 -0
- package/src/graphiti/provider-client.js +22 -1
- package/src/graphiti/source-marker.js +54 -3
- package/src/graphiti/task-knowledge-recall.js +250 -0
- package/src/http/api-router.js +23 -108
- package/src/http/external-knowledge-api-router.js +104 -0
- package/src/http/graph-api-router.js +12 -1
- package/src/http/request-policy.js +1 -1
- package/src/local-runtime/lifecycle.js +26 -15
- package/src/mcp/create-mcp-server.js +1 -0
- package/src/mcp/instructions.js +7 -7
- package/src/mcp/tool-annotations.js +6 -0
- package/src/mcp/tool-result.js +1 -0
- package/src/server/application-lifecycle.js +2 -11
- package/src/server/blocked-ports.js +11 -0
- package/src/server.js +36 -17
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +2 -1
- package/src/setup/claude-code-config.js +2 -1
- package/src/setup/codex-bootstrap.js +12 -7
- package/src/setup/container-runtime.js +25 -21
- package/src/setup/graph-runtime.js +148 -56
- package/src/setup/node-runtime.js +3 -3
- package/src/setup/options.js +2 -4
- package/src/setup/paths.js +18 -3
- package/src/setup/review-skill-adapters.js +49 -0
- package/src/setup/setup.js +26 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +317 -0
- package/src/system/runtime-settings.js +131 -0
- package/src/system/system-service.js +119 -0
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
- package/dist/web/assets/console-B_Ena2DS.js +0 -5
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-CjBwYSMX.js +0 -1
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- package/dist/web/assets/index-DAzeFJSc.js +0 -2
- package/dist/web/assets/model-C-5RVIJ2.js +0 -1
- package/dist/web/assets/paths-BkULqmGI.js +0 -1
- package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
- package/src/agent/candidate-read-service.js +0 -53
- package/src/agent/context-pack-service.js +0 -83
- package/src/agent/public-read-policy.js +0 -39
- package/src/agent/public-read-service.js +0 -69
- package/src/agent/read-projection.js +0 -107
- package/src/agent/search-service.js +0 -61
- package/src/agent/space-boundary.js +0 -33
- package/src/app/bootstrap.js +0 -8
- package/src/app/create-application.js +0 -143
- package/src/app/state-service.js +0 -38
- package/src/candidates.js +0 -78
- package/src/classifier.js +0 -63
- package/src/cli/command-arguments.js +0 -14
- package/src/context-pack.js +0 -206
- package/src/extractor.js +0 -77
- package/src/fact-history.js +0 -39
- package/src/facts/fact-service.js +0 -89
- package/src/http/bounded-json-array.js +0 -19
- package/src/http/lens-route.js +0 -50
- package/src/ingestion.js +0 -161
- package/src/lens/lens-correction-service.js +0 -81
- package/src/lens/lens-fact-writer.js +0 -85
- package/src/lens/lens-input.js +0 -62
- package/src/lens/lens-query-projection.js +0 -87
- package/src/lens/lens-query-ranking.js +0 -62
- package/src/lens/lens-query.js +0 -143
- package/src/lens/lens-resource-projection.js +0 -93
- package/src/lens/lens-resource-selection.js +0 -45
- package/src/lens/lens-resource-service.js +0 -103
- package/src/lens/lens-search-query.js +0 -122
- package/src/lens/lens-service.js +0 -183
- package/src/models.js +0 -44
- package/src/observer.js +0 -83
- package/src/project-rules.js +0 -33
- package/src/publication/outbox-service.js +0 -46
- package/src/publication/publication-envelope-schema.js +0 -75
- package/src/publication/publication-envelope.js +0 -37
- package/src/publication/publication-policy.js +0 -82
- package/src/publication/publication-service.js +0 -48
- package/src/router.js +0 -58
- package/src/runtime-options.js +0 -138
- package/src/security/episode-visibility.js +0 -14
- package/src/security/fact-visibility.js +0 -15
- package/src/security/source-metadata-policy.js +0 -8
- package/src/storage/episode-evidence-preview.js +0 -100
- package/src/storage/fact-search-order.js +0 -18
- package/src/storage/file/episode-evidence.js +0 -37
- package/src/storage/file/episode-index.js +0 -59
- package/src/storage/file/fact-search.js +0 -60
- package/src/storage/file-store.js +0 -437
- package/src/storage/migrate.js +0 -128
- package/src/storage/migrations/001-initial.sql +0 -83
- package/src/storage/migrations/002-storage-integrity.sql +0 -113
- package/src/storage/record-validation.js +0 -46
- package/src/storage/snapshot-defaults.js +0 -54
- package/src/storage/sqlite/candidates-repository.js +0 -84
- package/src/storage/sqlite/episode-evidence-repository.js +0 -154
- package/src/storage/sqlite/episode-privacy-sql.js +0 -33
- package/src/storage/sqlite/episodes-repository.js +0 -52
- package/src/storage/sqlite/fact-search-repository.js +0 -162
- package/src/storage/sqlite/facts-repository.js +0 -146
- package/src/storage/sqlite/imports-repository.js +0 -39
- package/src/storage/sqlite/mapper.js +0 -84
- package/src/storage/sqlite/open-database.js +0 -15
- package/src/storage/sqlite/outbox-repository.js +0 -103
- package/src/storage/sqlite/snapshot-repository.js +0 -69
- package/src/storage/sqlite/spaces-repository.js +0 -52
- package/src/storage/sqlite/subscriptions-repository.js +0 -54
- package/src/storage/sqlite-store.js +0 -159
- package/src/storage/stable-json.js +0 -24
- package/src/storage/store-port.js +0 -20
- package/src/store.js +0 -2
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { detectSensitiveContent } from '../security/sensitive-content.js';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
externalDocumentEpisode,
|
|
7
|
+
externalDocumentHash,
|
|
8
|
+
externalDocumentStateKey,
|
|
9
|
+
normalizeExternalDocument
|
|
10
|
+
} from './document-mapping.js';
|
|
11
|
+
import { safeExternalSourceDiagnostic } from './safe-diagnostic.js';
|
|
12
|
+
|
|
13
|
+
const MODES = new Set(['hybrid', 'live', 'mirror']);
|
|
14
|
+
const MAX_TARGETS = 32;
|
|
15
|
+
const CREDENTIAL_FIELDS = new Set([
|
|
16
|
+
'token',
|
|
17
|
+
'accesstoken',
|
|
18
|
+
'refreshtoken',
|
|
19
|
+
'authtoken',
|
|
20
|
+
'apikey',
|
|
21
|
+
'secret',
|
|
22
|
+
'clientsecret',
|
|
23
|
+
'appsecret',
|
|
24
|
+
'password',
|
|
25
|
+
'passwd',
|
|
26
|
+
'passphrase',
|
|
27
|
+
'credential',
|
|
28
|
+
'credentials',
|
|
29
|
+
'authorization',
|
|
30
|
+
'cookie',
|
|
31
|
+
'privatekey',
|
|
32
|
+
'signingkey',
|
|
33
|
+
'clientkey'
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export class ExternalKnowledgeService {
|
|
37
|
+
constructor({
|
|
38
|
+
app,
|
|
39
|
+
registry,
|
|
40
|
+
connectors,
|
|
41
|
+
env = process.env,
|
|
42
|
+
now = () => new Date(),
|
|
43
|
+
createId = randomUUID
|
|
44
|
+
}) {
|
|
45
|
+
if (!app || !registry || !connectors) {
|
|
46
|
+
throw new TypeError('External knowledge service requires app, registry, and connectors');
|
|
47
|
+
}
|
|
48
|
+
this.app = app;
|
|
49
|
+
this.registry = registry;
|
|
50
|
+
this.connectors = connectors;
|
|
51
|
+
this.env = env;
|
|
52
|
+
this.now = now;
|
|
53
|
+
this.createId = createId;
|
|
54
|
+
this.syncs = new Map();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listConnectorTypes() {
|
|
58
|
+
return this.connectors.list?.() ?? [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async listBindings() {
|
|
62
|
+
return this.registry.list().map(publicBinding);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async discover(input) {
|
|
66
|
+
const connectorType = requiredString(input?.connectorType, 'connectorType', 64);
|
|
67
|
+
const connector = this.#connector(connectorType);
|
|
68
|
+
if (typeof connector.discover !== 'function') {
|
|
69
|
+
throw new TypeError(`${connectorType} does not support discovery`);
|
|
70
|
+
}
|
|
71
|
+
return connector.discover({
|
|
72
|
+
...this.#context({
|
|
73
|
+
connectorConfig: safeConfiguration(input.connectorConfig ?? {}),
|
|
74
|
+
source: safeConfiguration(input.source ?? {})
|
|
75
|
+
}),
|
|
76
|
+
query: typeof input.query === 'string' ? input.query : null,
|
|
77
|
+
cursor: typeof input.cursor === 'string' ? input.cursor : null,
|
|
78
|
+
limit: positiveInteger(input.limit ?? 50, 'limit', 100)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async createBinding(input) {
|
|
83
|
+
const connectorType = requiredString(input?.connectorType, 'connectorType', 64);
|
|
84
|
+
const connector = this.#connector(connectorType);
|
|
85
|
+
const connectorConfig = safeConfiguration(input.connectorConfig ?? {});
|
|
86
|
+
const source = safeConfiguration(input.source ?? {});
|
|
87
|
+
const id = this.createId();
|
|
88
|
+
const targets = bindingTargets(input, { bindingId: id });
|
|
89
|
+
assertConnectorMethods(connectorType, connector, targets);
|
|
90
|
+
await this.#assertPersonalProjects(targets);
|
|
91
|
+
const checked = typeof connector.check === 'function'
|
|
92
|
+
? await connector.check(this.#context({
|
|
93
|
+
id,
|
|
94
|
+
connectorConfig,
|
|
95
|
+
source,
|
|
96
|
+
targets
|
|
97
|
+
}))
|
|
98
|
+
: { status: 'ready' };
|
|
99
|
+
if (checked?.status && checked.status !== 'ready') {
|
|
100
|
+
throw new Error(checked.message ?? `${connectorType} connection is not ready`);
|
|
101
|
+
}
|
|
102
|
+
assertNegotiatedCapabilities(connectorType, checked?.capabilities, targets);
|
|
103
|
+
const timestamp = this.now().toISOString();
|
|
104
|
+
const binding = {
|
|
105
|
+
id,
|
|
106
|
+
name: requiredString(input.name, 'name', 120),
|
|
107
|
+
connectorType,
|
|
108
|
+
connectorConfig,
|
|
109
|
+
source,
|
|
110
|
+
targets,
|
|
111
|
+
status: 'ready',
|
|
112
|
+
capabilities: checked?.capabilities ?? connector.capabilities ?? [],
|
|
113
|
+
createdAt: timestamp,
|
|
114
|
+
updatedAt: timestamp
|
|
115
|
+
};
|
|
116
|
+
return publicBinding(this.registry.put(binding));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async updateBindingTargets(id, input) {
|
|
120
|
+
const binding = this.#binding(id);
|
|
121
|
+
const connector = this.#connector(binding.connectorType);
|
|
122
|
+
const targets = bindingTargets(input, {
|
|
123
|
+
bindingId: binding.id,
|
|
124
|
+
existingTargets: binding.targets,
|
|
125
|
+
defaultMode: commonBindingMode(binding.targets)
|
|
126
|
+
});
|
|
127
|
+
assertConnectorMethods(binding.connectorType, connector, targets);
|
|
128
|
+
await this.#assertPersonalProjects(targets);
|
|
129
|
+
const checked = typeof connector.check === 'function'
|
|
130
|
+
? await connector.check(this.#context({ ...binding, targets }))
|
|
131
|
+
: { status: binding.status, capabilities: binding.capabilities };
|
|
132
|
+
if (checked?.status && checked.status !== 'ready') {
|
|
133
|
+
throw new Error(checked.message ?? `${binding.connectorType} connection is not ready`);
|
|
134
|
+
}
|
|
135
|
+
const capabilities = checked?.capabilities ?? binding.capabilities;
|
|
136
|
+
assertNegotiatedCapabilities(binding.connectorType, capabilities, targets);
|
|
137
|
+
|
|
138
|
+
const nextByKey = new Map(targets.map((target) => [targetKey(target), target]));
|
|
139
|
+
for (const previous of binding.targets) {
|
|
140
|
+
const next = nextByKey.get(targetKey(previous));
|
|
141
|
+
if (!next || (previous.mode !== 'live' && next.mode === 'live')) {
|
|
142
|
+
await this.#invalidateTarget(previous);
|
|
143
|
+
if (next) next.sync = emptySyncState();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
binding.targets = targets;
|
|
147
|
+
binding.status = checked?.status ?? binding.status;
|
|
148
|
+
binding.capabilities = capabilities ?? [];
|
|
149
|
+
binding.updatedAt = this.now().toISOString();
|
|
150
|
+
return publicBinding(this.registry.put(binding));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async checkBinding(id) {
|
|
154
|
+
const binding = this.#binding(id);
|
|
155
|
+
const connector = this.#connector(binding.connectorType);
|
|
156
|
+
const result = typeof connector.check === 'function'
|
|
157
|
+
? await connector.check(this.#context(binding))
|
|
158
|
+
: { status: 'ready', capabilities: connector.capabilities ?? [] };
|
|
159
|
+
assertNegotiatedCapabilities(binding.connectorType, result.capabilities, binding.targets);
|
|
160
|
+
binding.status = result.status ?? 'ready';
|
|
161
|
+
binding.capabilities = result.capabilities ?? binding.capabilities;
|
|
162
|
+
binding.updatedAt = this.now().toISOString();
|
|
163
|
+
this.registry.put(binding);
|
|
164
|
+
return { bindingId: binding.id, ...result };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async syncBinding(id, options = {}) {
|
|
168
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
169
|
+
throw new TypeError('Synchronization options must be an object');
|
|
170
|
+
}
|
|
171
|
+
const maxPages = positiveInteger(options.maxPages ?? 100, 'maxPages', 100);
|
|
172
|
+
const pageSize = positiveInteger(options.pageSize ?? 100, 'pageSize', 100);
|
|
173
|
+
const binding = this.#binding(id);
|
|
174
|
+
const targets = selectedTargets(binding, options, { capability: 'sync' });
|
|
175
|
+
const results = [];
|
|
176
|
+
for (const target of targets) {
|
|
177
|
+
const syncKey = `${id}:${target.id}`;
|
|
178
|
+
const pending = this.syncs.get(syncKey) ?? this.#sync(binding, target, {
|
|
179
|
+
maxPages,
|
|
180
|
+
pageSize
|
|
181
|
+
}).finally(() => this.syncs.delete(syncKey));
|
|
182
|
+
this.syncs.set(syncKey, pending);
|
|
183
|
+
results.push(await pending);
|
|
184
|
+
}
|
|
185
|
+
return aggregateSyncResults(id, results);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async retrieveBinding(id, input = {}) {
|
|
189
|
+
const { query, limit = 12 } = input;
|
|
190
|
+
const binding = this.#binding(id);
|
|
191
|
+
const target = selectedTargets(binding, input, {
|
|
192
|
+
capability: 'retrieve',
|
|
193
|
+
requireOne: true
|
|
194
|
+
})[0];
|
|
195
|
+
if (target.mode === 'mirror') {
|
|
196
|
+
throw new TypeError('This binding is configured for synchronized graph search only');
|
|
197
|
+
}
|
|
198
|
+
const connector = this.#connector(binding.connectorType);
|
|
199
|
+
if (typeof connector.retrieve !== 'function') {
|
|
200
|
+
throw new TypeError(`${binding.connectorType} does not support live retrieval`);
|
|
201
|
+
}
|
|
202
|
+
const normalizedQuery = requiredString(query, 'query', 2_000);
|
|
203
|
+
const resolvedLimit = positiveInteger(limit, 'limit', 100);
|
|
204
|
+
const result = await connector.retrieve({
|
|
205
|
+
...this.#context(binding, target),
|
|
206
|
+
query: normalizedQuery,
|
|
207
|
+
limit: resolvedLimit
|
|
208
|
+
});
|
|
209
|
+
const items = [];
|
|
210
|
+
let skippedCredentials = 0;
|
|
211
|
+
for (const value of (result?.items ?? result?.hits ?? []).slice(0, resolvedLimit)) {
|
|
212
|
+
try {
|
|
213
|
+
items.push(normalizeExternalDocument(value));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!isCredentialDocumentError(error)) throw error;
|
|
216
|
+
skippedCredentials += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
bindingId: id,
|
|
221
|
+
targetId: target.id,
|
|
222
|
+
personalProjectId: target.personalProjectId,
|
|
223
|
+
query: normalizedQuery,
|
|
224
|
+
items,
|
|
225
|
+
skippedCredentials
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async deleteBinding(id) {
|
|
230
|
+
const binding = this.#binding(id);
|
|
231
|
+
let invalidated = 0;
|
|
232
|
+
for (const target of binding.targets) {
|
|
233
|
+
invalidated += await this.#invalidateTarget(target);
|
|
234
|
+
}
|
|
235
|
+
this.registry.delete(id);
|
|
236
|
+
return { bindingId: id, status: 'deleted', invalidated };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
projectGraphProjection({ personalSpaceId, personalProjectId = null, graph }) {
|
|
240
|
+
const projectNodes = new Map(
|
|
241
|
+
(graph?.nodes ?? [])
|
|
242
|
+
.filter((node) => node?.type === 'PersonalProject')
|
|
243
|
+
.map((node) => [node?.attributes?.projectId, node])
|
|
244
|
+
.filter(([projectId]) => typeof projectId === 'string' && projectId)
|
|
245
|
+
);
|
|
246
|
+
const nodes = [];
|
|
247
|
+
const edges = [];
|
|
248
|
+
const projectedBindings = new Set();
|
|
249
|
+
|
|
250
|
+
for (const binding of this.registry.list()) {
|
|
251
|
+
const targets = binding.targets.filter((target) =>
|
|
252
|
+
target.personalSpaceId === personalSpaceId &&
|
|
253
|
+
(!personalProjectId || target.personalProjectId === personalProjectId) &&
|
|
254
|
+
projectNodes.has(target.personalProjectId)
|
|
255
|
+
);
|
|
256
|
+
if (!targets.length) continue;
|
|
257
|
+
|
|
258
|
+
const sourceId = `external-knowledge-source:${binding.id}`;
|
|
259
|
+
if (!projectedBindings.has(binding.id)) {
|
|
260
|
+
projectedBindings.add(binding.id);
|
|
261
|
+
const groupId = projectNodes.get(targets[0].personalProjectId)?.group_id;
|
|
262
|
+
nodes.push({
|
|
263
|
+
id: sourceId,
|
|
264
|
+
name: binding.name,
|
|
265
|
+
type: 'ExternalKnowledgeSource',
|
|
266
|
+
summary: `Read-only ${binding.connectorType} knowledge source.`,
|
|
267
|
+
...(groupId ? { group_id: groupId } : {}),
|
|
268
|
+
origin_quadrant: 'known_known',
|
|
269
|
+
current_quadrant: 'known_known',
|
|
270
|
+
epistemic_status: 'observed',
|
|
271
|
+
confirmation_status: 'confirmed',
|
|
272
|
+
confirmation_basis: {
|
|
273
|
+
existence_reason: 'This external knowledge connection is configured in FULI.',
|
|
274
|
+
quadrant_reason: 'The connection and its project assignments are explicit configuration.',
|
|
275
|
+
proposed_by: { kind: 'import', label: binding.connectorType },
|
|
276
|
+
confirmed_by: { kind: 'user', label: 'connection configuration' },
|
|
277
|
+
confirmed_at: binding.updatedAt ?? binding.createdAt ?? null
|
|
278
|
+
},
|
|
279
|
+
attributes: {
|
|
280
|
+
externalBindingId: binding.id,
|
|
281
|
+
externalConnectorType: binding.connectorType,
|
|
282
|
+
externalBindingStatus: binding.status,
|
|
283
|
+
externalCapabilities: binding.capabilities ?? [],
|
|
284
|
+
externalTargetCount: binding.targets.length,
|
|
285
|
+
externalModes: [...new Set(binding.targets.map(({ mode }) => mode))]
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const target of targets) {
|
|
291
|
+
const projectNode = projectNodes.get(target.personalProjectId);
|
|
292
|
+
edges.push({
|
|
293
|
+
id: `external-knowledge-binding:${binding.id}:${target.id}`,
|
|
294
|
+
source: projectNode.id,
|
|
295
|
+
target: sourceId,
|
|
296
|
+
source_name: projectNode.name,
|
|
297
|
+
target_name: binding.name,
|
|
298
|
+
type: 'USES_EXTERNAL_KNOWLEDGE',
|
|
299
|
+
fact: `${projectNode.name} uses the read-only external knowledge source ${binding.name}.`,
|
|
300
|
+
origin_quadrant: 'known_known',
|
|
301
|
+
current_quadrant: 'known_known',
|
|
302
|
+
epistemic_status: 'observed',
|
|
303
|
+
confirmation_status: 'confirmed',
|
|
304
|
+
attributes: {
|
|
305
|
+
externalBindingId: binding.id,
|
|
306
|
+
externalBindingTargetId: target.id,
|
|
307
|
+
mode: target.mode,
|
|
308
|
+
status: target.status,
|
|
309
|
+
lastSyncedAt: target.sync?.lastSyncedAt ?? null
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { nodes, edges };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async #sync(binding, target, { maxPages, pageSize }) {
|
|
318
|
+
if (target.mode === 'live') {
|
|
319
|
+
throw new TypeError('This binding is configured for live retrieval only');
|
|
320
|
+
}
|
|
321
|
+
const connector = this.#connector(binding.connectorType);
|
|
322
|
+
let cursor = target.sync.cursor;
|
|
323
|
+
let imported = 0;
|
|
324
|
+
let unchanged = 0;
|
|
325
|
+
let invalidated = 0;
|
|
326
|
+
let skippedCredentials = 0;
|
|
327
|
+
let hasMore = false;
|
|
328
|
+
try {
|
|
329
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
330
|
+
const page = connectorSyncPage(await connector.sync({
|
|
331
|
+
...this.#context(binding, target),
|
|
332
|
+
cursor,
|
|
333
|
+
limit: pageSize
|
|
334
|
+
}), pageSize);
|
|
335
|
+
const { items, deleted } = page;
|
|
336
|
+
for (const value of items) {
|
|
337
|
+
let document;
|
|
338
|
+
try {
|
|
339
|
+
document = normalizeExternalDocument(value);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (!isCredentialDocumentError(error)) throw error;
|
|
342
|
+
skippedCredentials += 1;
|
|
343
|
+
const rejectedId = connectorItemId(value);
|
|
344
|
+
if (rejectedId) {
|
|
345
|
+
const rejectedKey = externalDocumentStateKey(rejectedId);
|
|
346
|
+
const previous = target.sync.items[rejectedKey];
|
|
347
|
+
if (previous) {
|
|
348
|
+
invalidated += await this.#invalidateEntities(
|
|
349
|
+
previous.entityIds ?? [],
|
|
350
|
+
target
|
|
351
|
+
);
|
|
352
|
+
delete target.sync.items[rejectedKey];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const hash = externalDocumentHash(document);
|
|
358
|
+
const itemKey = externalDocumentStateKey(document.id);
|
|
359
|
+
const previous = target.sync.items[itemKey];
|
|
360
|
+
if (previous?.hash === hash) {
|
|
361
|
+
unchanged += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const capture = await this.app.captureSessionKnowledge(externalDocumentEpisode({
|
|
365
|
+
binding: bindingForTarget(binding, target),
|
|
366
|
+
document,
|
|
367
|
+
now: this.now()
|
|
368
|
+
}));
|
|
369
|
+
if (capture?.status === 'capture_disabled') {
|
|
370
|
+
throw new Error('FULI knowledge capture is disabled');
|
|
371
|
+
}
|
|
372
|
+
invalidated += await this.#invalidateEntities(
|
|
373
|
+
previous?.entityIds ?? [],
|
|
374
|
+
target
|
|
375
|
+
);
|
|
376
|
+
target.sync.items[itemKey] = {
|
|
377
|
+
externalId: document.id,
|
|
378
|
+
hash,
|
|
379
|
+
entityIds: capture?.entity_ids ?? capture?.entityIds ?? [],
|
|
380
|
+
updatedAt: document.updatedAt,
|
|
381
|
+
url: document.url
|
|
382
|
+
};
|
|
383
|
+
imported += 1;
|
|
384
|
+
}
|
|
385
|
+
for (const value of deleted) {
|
|
386
|
+
const deletedId = connectorItemId(value);
|
|
387
|
+
if (!deletedId) continue;
|
|
388
|
+
const itemKey = externalDocumentStateKey(deletedId);
|
|
389
|
+
if (!target.sync.items[itemKey]) continue;
|
|
390
|
+
invalidated += await this.#invalidateEntities(
|
|
391
|
+
target.sync.items[itemKey].entityIds ?? [],
|
|
392
|
+
target
|
|
393
|
+
);
|
|
394
|
+
delete target.sync.items[itemKey];
|
|
395
|
+
}
|
|
396
|
+
hasMore = page?.hasMore === true;
|
|
397
|
+
const nextCursor = page?.nextCursor ?? null;
|
|
398
|
+
if (hasMore && (!nextCursor || nextCursor === cursor)) {
|
|
399
|
+
throw new TypeError('Connector returned an invalid pagination cursor');
|
|
400
|
+
}
|
|
401
|
+
cursor = nextCursor;
|
|
402
|
+
if (!hasMore) break;
|
|
403
|
+
if (pageNumber === maxPages - 1) {
|
|
404
|
+
throw new Error(`Synchronization exceeded ${maxPages} pages`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
target.status = 'ready';
|
|
408
|
+
binding.updatedAt = this.now().toISOString();
|
|
409
|
+
target.sync.cursor = cursor;
|
|
410
|
+
target.sync.lastSyncedAt = binding.updatedAt;
|
|
411
|
+
target.sync.error = null;
|
|
412
|
+
target.sync.skippedCredentials = skippedCredentials;
|
|
413
|
+
this.registry.put(binding);
|
|
414
|
+
return {
|
|
415
|
+
bindingId: binding.id,
|
|
416
|
+
targetId: target.id,
|
|
417
|
+
personalProjectId: target.personalProjectId,
|
|
418
|
+
status: 'ready',
|
|
419
|
+
imported,
|
|
420
|
+
unchanged,
|
|
421
|
+
invalidated,
|
|
422
|
+
skippedCredentials,
|
|
423
|
+
nextCursor: cursor,
|
|
424
|
+
hasMore
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
target.status = 'error';
|
|
428
|
+
binding.updatedAt = this.now().toISOString();
|
|
429
|
+
target.sync.error = safeExternalSourceDiagnostic(error);
|
|
430
|
+
this.registry.put(binding);
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async #invalidateTarget(target) {
|
|
436
|
+
let invalidated = 0;
|
|
437
|
+
for (const item of Object.values(target.sync?.items ?? {})) {
|
|
438
|
+
invalidated += await this.#invalidateEntities(item.entityIds ?? [], target);
|
|
439
|
+
}
|
|
440
|
+
return invalidated;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async #invalidateEntities(ids, target) {
|
|
444
|
+
if (!ids.length) return 0;
|
|
445
|
+
if (typeof this.app.reviseKnowledgeItem !== 'function') {
|
|
446
|
+
throw new Error('FULI application cannot invalidate replaced external knowledge');
|
|
447
|
+
}
|
|
448
|
+
for (const itemId of ids) {
|
|
449
|
+
await this.app.reviseKnowledgeItem({
|
|
450
|
+
personalSpaceId: target.personalSpaceId,
|
|
451
|
+
personalProjectId: target.personalProjectId,
|
|
452
|
+
itemKind: 'entity',
|
|
453
|
+
itemId,
|
|
454
|
+
action: 'invalidate',
|
|
455
|
+
reason: 'The read-only external source was changed, removed, or disconnected.',
|
|
456
|
+
operationActor: 'agent'
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return ids.length;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async #assertPersonalProjects(targets) {
|
|
463
|
+
const bySpace = new Map();
|
|
464
|
+
for (const target of targets) {
|
|
465
|
+
const selected = bySpace.get(target.personalSpaceId) ?? [];
|
|
466
|
+
selected.push(target);
|
|
467
|
+
bySpace.set(target.personalSpaceId, selected);
|
|
468
|
+
}
|
|
469
|
+
for (const [personalSpaceId, selected] of bySpace) {
|
|
470
|
+
const projects = await this.app.listPersonalProjects({ personalSpaceId });
|
|
471
|
+
const available = new Set(projects.map(({ project_id: id, id: fallback }) => id ?? fallback));
|
|
472
|
+
if (selected.some(({ personalProjectId }) => !available.has(personalProjectId))) {
|
|
473
|
+
throw new TypeError('External knowledge target must be an existing personal project');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#binding(id) {
|
|
479
|
+
const binding = this.registry.get(requiredString(id, 'binding id', 128));
|
|
480
|
+
if (!binding) throw new TypeError('External knowledge binding was not found');
|
|
481
|
+
return binding;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#connector(type) {
|
|
485
|
+
const connector = this.connectors.get(type);
|
|
486
|
+
if (!connector) throw new TypeError(`Unknown external knowledge connector: ${type}`);
|
|
487
|
+
return connector;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#context(binding, target = null) {
|
|
491
|
+
return {
|
|
492
|
+
config: binding.connectorConfig,
|
|
493
|
+
source: binding.source,
|
|
494
|
+
env: this.env,
|
|
495
|
+
bindingId: binding.id ?? null,
|
|
496
|
+
mode: target?.mode ?? commonBindingMode(binding.targets ?? [])
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function publicBinding(binding) {
|
|
502
|
+
const output = structuredClone(binding);
|
|
503
|
+
const first = output.targets?.[0] ?? null;
|
|
504
|
+
return {
|
|
505
|
+
...output,
|
|
506
|
+
// Keep the v1 aliases while clients migrate to target-aware bindings.
|
|
507
|
+
target: first ? {
|
|
508
|
+
personalSpaceId: first.personalSpaceId,
|
|
509
|
+
personalProjectId: first.personalProjectId
|
|
510
|
+
} : null,
|
|
511
|
+
mode: first?.mode ?? null,
|
|
512
|
+
sync: first?.sync ?? null
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function bindingTargets(input, {
|
|
517
|
+
bindingId,
|
|
518
|
+
existingTargets = [],
|
|
519
|
+
defaultMode = 'hybrid'
|
|
520
|
+
}) {
|
|
521
|
+
const values = Array.isArray(input?.targets)
|
|
522
|
+
? input.targets
|
|
523
|
+
: input?.target
|
|
524
|
+
? [{ ...input.target, mode: input.mode ?? defaultMode }]
|
|
525
|
+
: null;
|
|
526
|
+
if (!values || !values.length) {
|
|
527
|
+
throw new TypeError('At least one target personal project is required');
|
|
528
|
+
}
|
|
529
|
+
if (values.length > MAX_TARGETS) {
|
|
530
|
+
throw new TypeError(`A connection can target at most ${MAX_TARGETS} personal projects`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const existingByKey = new Map(existingTargets.map((target) => [targetKey(target), target]));
|
|
534
|
+
const reservedIds = new Set(existingTargets.map(({ id }) => id));
|
|
535
|
+
const seen = new Set();
|
|
536
|
+
return values.map((value, index) => {
|
|
537
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
538
|
+
throw new TypeError(`targets[${index}] must be an object`);
|
|
539
|
+
}
|
|
540
|
+
const target = {
|
|
541
|
+
personalSpaceId: requiredString(
|
|
542
|
+
value.personalSpaceId,
|
|
543
|
+
`targets[${index}].personalSpaceId`,
|
|
544
|
+
256
|
|
545
|
+
),
|
|
546
|
+
personalProjectId: requiredString(
|
|
547
|
+
value.personalProjectId,
|
|
548
|
+
`targets[${index}].personalProjectId`,
|
|
549
|
+
256
|
|
550
|
+
)
|
|
551
|
+
};
|
|
552
|
+
const key = targetKey(target);
|
|
553
|
+
if (seen.has(key)) throw new TypeError('Target personal projects must be unique');
|
|
554
|
+
seen.add(key);
|
|
555
|
+
const previous = existingByKey.get(key);
|
|
556
|
+
const mode = requiredString(
|
|
557
|
+
value.mode ?? previous?.mode ?? defaultMode,
|
|
558
|
+
`targets[${index}].mode`,
|
|
559
|
+
16
|
|
560
|
+
);
|
|
561
|
+
if (!MODES.has(mode)) throw new TypeError('mode must be hybrid, live, or mirror');
|
|
562
|
+
const id = previous?.id ?? targetId(bindingId, target, {
|
|
563
|
+
preferBindingId: existingTargets.length === 0 && index === 0,
|
|
564
|
+
reservedIds
|
|
565
|
+
});
|
|
566
|
+
reservedIds.add(id);
|
|
567
|
+
return {
|
|
568
|
+
id,
|
|
569
|
+
...target,
|
|
570
|
+
mode,
|
|
571
|
+
status: previous?.status ?? 'ready',
|
|
572
|
+
sync: structuredClone(previous?.sync ?? emptySyncState())
|
|
573
|
+
};
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function targetId(bindingId, target, { preferBindingId, reservedIds }) {
|
|
578
|
+
if (preferBindingId && !reservedIds.has(bindingId)) return bindingId;
|
|
579
|
+
const digest = createHash('sha256')
|
|
580
|
+
.update(`${bindingId}\0${target.personalSpaceId}\0${target.personalProjectId}`)
|
|
581
|
+
.digest('hex')
|
|
582
|
+
.slice(0, 24);
|
|
583
|
+
const id = `target:${digest}`;
|
|
584
|
+
if (reservedIds.has(id)) {
|
|
585
|
+
throw new TypeError('Target identifier collision');
|
|
586
|
+
}
|
|
587
|
+
return id;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function targetKey(target) {
|
|
591
|
+
return `${target.personalSpaceId}\0${target.personalProjectId}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function emptySyncState() {
|
|
595
|
+
return {
|
|
596
|
+
cursor: null,
|
|
597
|
+
lastSyncedAt: null,
|
|
598
|
+
error: null,
|
|
599
|
+
skippedCredentials: 0,
|
|
600
|
+
items: {}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function commonBindingMode(targets) {
|
|
605
|
+
const modes = new Set((targets ?? []).map(({ mode }) => mode));
|
|
606
|
+
if (modes.size === 1) return [...modes][0];
|
|
607
|
+
const requiresSync = [...modes].some((mode) => mode !== 'live');
|
|
608
|
+
const requiresRetrieve = [...modes].some((mode) => mode !== 'mirror');
|
|
609
|
+
if (requiresSync && requiresRetrieve) return 'hybrid';
|
|
610
|
+
if (requiresSync) return 'mirror';
|
|
611
|
+
return 'live';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function assertConnectorMethods(type, connector, targets) {
|
|
615
|
+
if (targets.some(({ mode }) => mode !== 'live') && typeof connector.sync !== 'function') {
|
|
616
|
+
throw new TypeError(`${type} does not support synchronization`);
|
|
617
|
+
}
|
|
618
|
+
if (targets.some(({ mode }) => mode !== 'mirror') && typeof connector.retrieve !== 'function') {
|
|
619
|
+
throw new TypeError(`${type} does not support live retrieval`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function assertNegotiatedCapabilities(type, capabilities, targets) {
|
|
624
|
+
if (!Array.isArray(capabilities) || !capabilities.length) return;
|
|
625
|
+
if (targets.some(({ mode }) => mode !== 'live') && !capabilities.includes('sync')) {
|
|
626
|
+
throw new TypeError(`${type} does not advertise synchronization capability`);
|
|
627
|
+
}
|
|
628
|
+
if (targets.some(({ mode }) => mode !== 'mirror') && !capabilities.includes('retrieve')) {
|
|
629
|
+
throw new TypeError(`${type} does not advertise live retrieval capability`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function selectedTargets(binding, input, { capability, requireOne = false }) {
|
|
634
|
+
const personalSpaceId = optionalString(input?.personalSpaceId, 'personalSpaceId', 256);
|
|
635
|
+
const personalProjectId = optionalString(input?.personalProjectId, 'personalProjectId', 256);
|
|
636
|
+
let targets = binding.targets.filter((target) =>
|
|
637
|
+
(!personalSpaceId || target.personalSpaceId === personalSpaceId) &&
|
|
638
|
+
(!personalProjectId || target.personalProjectId === personalProjectId)
|
|
639
|
+
);
|
|
640
|
+
if ((personalSpaceId || personalProjectId) && !targets.length) {
|
|
641
|
+
throw new TypeError('External knowledge binding is not assigned to this personal project');
|
|
642
|
+
}
|
|
643
|
+
targets = targets.filter(({ mode }) => capability === 'sync' ? mode !== 'live' : mode !== 'mirror');
|
|
644
|
+
if (!targets.length) {
|
|
645
|
+
throw new TypeError(capability === 'sync'
|
|
646
|
+
? 'This binding is configured for live retrieval only'
|
|
647
|
+
: 'This binding is configured for synchronized graph search only');
|
|
648
|
+
}
|
|
649
|
+
if (requireOne && targets.length !== 1) {
|
|
650
|
+
throw new TypeError('personalSpaceId and personalProjectId are required for a multi-project binding');
|
|
651
|
+
}
|
|
652
|
+
return targets;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function aggregateSyncResults(bindingId, results) {
|
|
656
|
+
if (results.length === 1) {
|
|
657
|
+
const { targetId: _targetId, personalProjectId: _projectId, ...result } = results[0];
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
bindingId,
|
|
662
|
+
status: results.every(({ status }) => status === 'ready') ? 'ready' : 'partial',
|
|
663
|
+
imported: sum(results, 'imported'),
|
|
664
|
+
unchanged: sum(results, 'unchanged'),
|
|
665
|
+
invalidated: sum(results, 'invalidated'),
|
|
666
|
+
skippedCredentials: sum(results, 'skippedCredentials'),
|
|
667
|
+
nextCursor: null,
|
|
668
|
+
hasMore: results.some(({ hasMore }) => hasMore),
|
|
669
|
+
targets: results
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function sum(values, field) {
|
|
674
|
+
return values.reduce((total, value) => total + (value[field] ?? 0), 0);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function bindingForTarget(binding, target) {
|
|
678
|
+
return {
|
|
679
|
+
...binding,
|
|
680
|
+
targetId: target.id,
|
|
681
|
+
target: {
|
|
682
|
+
personalSpaceId: target.personalSpaceId,
|
|
683
|
+
personalProjectId: target.personalProjectId
|
|
684
|
+
},
|
|
685
|
+
mode: target.mode,
|
|
686
|
+
sync: target.sync
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function safeConfiguration(value, path = 'configuration') {
|
|
691
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
692
|
+
throw new TypeError(`${path} must be an object`);
|
|
693
|
+
}
|
|
694
|
+
const output = Object.create(null);
|
|
695
|
+
for (const [key, item] of Object.entries(value)) {
|
|
696
|
+
if (credentialField(key) && !credentialReferenceField(key)) {
|
|
697
|
+
throw new TypeError(`${path}.${key} cannot contain credentials; use an environment variable name`);
|
|
698
|
+
}
|
|
699
|
+
if (item === null || typeof item === 'boolean' || typeof item === 'number') {
|
|
700
|
+
output[key] = item;
|
|
701
|
+
} else if (typeof item === 'string') {
|
|
702
|
+
assertSafeConfigurationString(item, `${path}.${key}`);
|
|
703
|
+
output[key] = item;
|
|
704
|
+
} else if (Array.isArray(item)) {
|
|
705
|
+
output[key] = item.map((entry, index) =>
|
|
706
|
+
entry && typeof entry === 'object'
|
|
707
|
+
? safeConfiguration(entry, `${path}.${key}[${index}]`)
|
|
708
|
+
: entry
|
|
709
|
+
);
|
|
710
|
+
} else if (item && typeof item === 'object') {
|
|
711
|
+
output[key] = safeConfiguration(item, `${path}.${key}`);
|
|
712
|
+
} else {
|
|
713
|
+
throw new TypeError(`${path}.${key} must be JSON-compatible`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (detectSensitiveContent(JSON.stringify(output)).restricted) {
|
|
717
|
+
throw new TypeError(`${path} cannot contain credentials; use an environment variable name`);
|
|
718
|
+
}
|
|
719
|
+
return output;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function assertSafeConfigurationString(value, path) {
|
|
723
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(value)) return;
|
|
724
|
+
try {
|
|
725
|
+
const url = new URL(value);
|
|
726
|
+
if (url.username || url.password) {
|
|
727
|
+
throw new TypeError(`${path} cannot contain credentials; use an environment variable name`);
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error instanceof TypeError && /cannot contain credentials/u.test(error.message)) {
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function credentialField(key) {
|
|
737
|
+
const normalized = normalizedConfigurationKey(key);
|
|
738
|
+
const withoutReferenceSuffix = normalized.replace(/(?:environment|env|names|name)$/u, '');
|
|
739
|
+
return CREDENTIAL_FIELDS.has(normalized) || CREDENTIAL_FIELDS.has(withoutReferenceSuffix);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function credentialReferenceField(key) {
|
|
743
|
+
return /(?:environment|env|names|name)$/u.test(normalizedConfigurationKey(key));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function normalizedConfigurationKey(key) {
|
|
747
|
+
return key.toLocaleLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function requiredString(value, label, maximum) {
|
|
751
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
|
|
752
|
+
const normalized = value.trim();
|
|
753
|
+
if (normalized.length > maximum) throw new TypeError(`${label} is too long`);
|
|
754
|
+
return normalized;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function optionalString(value, label, maximum) {
|
|
758
|
+
if (value === undefined || value === null || value === '') return null;
|
|
759
|
+
return requiredString(value, label, maximum);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function positiveInteger(value, label, maximum) {
|
|
763
|
+
if (!Number.isInteger(value) || value <= 0 || value > maximum) {
|
|
764
|
+
throw new TypeError(`${label} must be a positive integer no greater than ${maximum}`);
|
|
765
|
+
}
|
|
766
|
+
return value;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function isCredentialDocumentError(error) {
|
|
770
|
+
return error instanceof TypeError &&
|
|
771
|
+
error.message === 'Connector item contains credentials and cannot be retrieved or stored';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function connectorItemId(value) {
|
|
775
|
+
const id = typeof value === 'string' ? value : value?.id;
|
|
776
|
+
if (typeof id !== 'string') return null;
|
|
777
|
+
const normalized = id.trim();
|
|
778
|
+
return normalized && normalized.length <= 512 ? normalized : null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function connectorSyncPage(value, pageSize) {
|
|
782
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
783
|
+
throw new TypeError('Connector synchronization result must be an object');
|
|
784
|
+
}
|
|
785
|
+
const items = value.items ?? [];
|
|
786
|
+
const deleted = value.deleted ?? [];
|
|
787
|
+
if (!Array.isArray(items) || !Array.isArray(deleted)) {
|
|
788
|
+
throw new TypeError('Connector synchronization items and deleted must be arrays');
|
|
789
|
+
}
|
|
790
|
+
if (items.length > pageSize) {
|
|
791
|
+
throw new TypeError(`Connector returned more than ${pageSize} items`);
|
|
792
|
+
}
|
|
793
|
+
if (deleted.length > pageSize) {
|
|
794
|
+
throw new TypeError(`Connector returned more than ${pageSize} deletions`);
|
|
795
|
+
}
|
|
796
|
+
if (value.hasMore !== undefined && typeof value.hasMore !== 'boolean') {
|
|
797
|
+
throw new TypeError('Connector hasMore must be a boolean');
|
|
798
|
+
}
|
|
799
|
+
const nextCursor = value.nextCursor ?? null;
|
|
800
|
+
if (nextCursor !== null &&
|
|
801
|
+
(typeof nextCursor !== 'string' || nextCursor.length > 64_000)) {
|
|
802
|
+
throw new TypeError('Connector nextCursor must be a string no longer than 64000 characters');
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
...value,
|
|
806
|
+
items,
|
|
807
|
+
deleted,
|
|
808
|
+
hasMore: value.hasMore === true,
|
|
809
|
+
nextCursor
|
|
810
|
+
};
|
|
811
|
+
}
|