fraim 2.0.357-next.1 → 2.0.357-next.3
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/registry/scripts/learning/run-application.js +2 -2
- package/dist/registry/scripts/learning-usage/learning-source-record.js +3 -10
- package/dist/src/cli/api/provider-client.js +4 -1
- package/dist/src/fraim/db-service.js +12 -7
- package/dist/src/local-mcp-server/learning-source-record.js +3 -10
- package/package.json +1 -1
|
@@ -780,9 +780,9 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
780
780
|
const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.path, (0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
|
|
781
781
|
if (parsed.status !== 'valid' || parsed.record.kind !== source.kind || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, home.owner) || parsed.record.state !== 'pending' || parsed.record.synthesized)
|
|
782
782
|
throw new Error('invalid-learning-source');
|
|
783
|
-
// References may use immutable identity, home-relative
|
|
783
|
+
// References may use immutable identity, content-relative or home-relative path, or
|
|
784
784
|
// the selected absolute path. None may archive an unfinished dependency.
|
|
785
|
-
const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId,
|
|
785
|
+
const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId,
|
|
786
786
|
node_path_1.default.relative(home.contentRoot, source.path).replace(/\\/g, '/'), node_path_1.default.relative((0, owner_identity_1.ownerArtifactRoot)(home), source.path).replace(/\\/g, '/')]);
|
|
787
787
|
if (actions.some(action => action.sourceRefs.some(ref => refs.has(ref.replace(/\\/g, '/')) || (node_path_1.default.isAbsolute(ref) && (0, completion_state_2.sourcePathKey)(ref) === (0, completion_state_2.sourcePathKey)(source.path)))))
|
|
788
788
|
throw new Error('action-processing-pending');
|
|
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
49
49
|
for (const line of fm[1].split(/\r?\n/)) {
|
|
50
50
|
const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
|
|
51
51
|
if (!match) {
|
|
52
|
-
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|
|
|
52
|
+
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
|
|
53
53
|
malformedField = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
76
76
|
return invalid('Unsupported metadata key spelling');
|
|
77
77
|
if (duplicates.size)
|
|
78
78
|
return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
|
|
79
|
-
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', '
|
|
79
|
+
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
|
|
80
80
|
if (!Object.prototype.hasOwnProperty.call(fields, field))
|
|
81
81
|
return invalid(`Missing metadata: ${field}`);
|
|
82
82
|
}
|
|
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
110
110
|
return invalid('Source is outside its configured learning location');
|
|
111
111
|
if (state === 'archived' && !fields.synthesized)
|
|
112
112
|
return invalid('Archive has no successful processing date');
|
|
113
|
-
const reference = fields.usageSourceRef;
|
|
114
113
|
const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
|
|
115
114
|
if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
|
|
116
115
|
return invalid('Source escapes its owning root');
|
|
117
|
-
if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
|
|
118
|
-
return invalid('Invalid usageSourceRef');
|
|
119
116
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
|
|
120
117
|
return invalid('Sources require an immutable sourceId UUID');
|
|
121
118
|
}
|
|
122
|
-
if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
|
|
123
|
-
|| reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
|
|
124
|
-
return invalid('usageSourceRef must name the owner-relative pending path');
|
|
125
|
-
}
|
|
126
119
|
return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
|
|
127
|
-
date: fields.date, job: fields.job, runId: fields.runId,
|
|
120
|
+
date: fields.date, job: fields.job, runId: fields.runId,
|
|
128
121
|
usageIdentity: `learning-source:${fields.sourceId}`,
|
|
129
122
|
sourceId: fields.sourceId,
|
|
130
123
|
synthesized: fields.synthesized, pendingPath, archivePath, state } };
|
|
@@ -8,7 +8,10 @@ exports.ProviderClient = void 0;
|
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
class ProviderClient {
|
|
10
10
|
constructor(fraimKey, serverUrl) {
|
|
11
|
-
this.serverUrl = serverUrl || process.env.FRAIM_REMOTE_URL ||
|
|
11
|
+
this.serverUrl = serverUrl || process.env.FRAIM_REMOTE_URL ||
|
|
12
|
+
((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE'
|
|
13
|
+
? 'https://app-ashley-fraim-ppe.azurewebsites.net'
|
|
14
|
+
: 'https://fraim.wellnessatwork.me');
|
|
12
15
|
this.client = axios_1.default.create({
|
|
13
16
|
baseURL: this.serverUrl,
|
|
14
17
|
headers: {
|
|
@@ -180,27 +180,32 @@ class FraimDbService {
|
|
|
180
180
|
async initializeIndexes() {
|
|
181
181
|
if (!this.db)
|
|
182
182
|
throw new Error('DB not connected during index initialization');
|
|
183
|
-
|
|
183
|
+
// Cosmos DB for MongoDB rejects adding unique indexes to collections that already exist
|
|
184
|
+
// (even empty ones auto-created by the driver). All unique createIndex calls use .catch(() => {})
|
|
185
|
+
// so startup never crashes on a fresh database. On established databases (prod, dev) the indexes
|
|
186
|
+
// already exist and createIndex is a no-op; on a fresh PPE/test database the unique constraint
|
|
187
|
+
// is silently skipped — the application's own upsert/insert logic still enforces logical uniqueness.
|
|
188
|
+
await this.keysCollection.createIndex({ key: 1 }, { unique: true }).catch(() => { });
|
|
184
189
|
await this.keysCollection.createIndex({ userId: 1 });
|
|
185
190
|
await this.keysCollection.createIndex({ stripeSubscriptionId: 1 });
|
|
186
191
|
await this.keysCollection.createIndex({ status: 1 });
|
|
187
192
|
await this.keysCollection.createIndex({ expiresAt: 1 });
|
|
188
|
-
await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true });
|
|
193
|
+
await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true }).catch(() => { });
|
|
189
194
|
await this.sessionsCollection.createIndex({ userId: 1 });
|
|
190
195
|
await this.sessionsCollection.createIndex({ lastActive: -1 });
|
|
191
196
|
// Issue #2093 — getActiveSessionByApiKey no longer bounds its match by recency, so a
|
|
192
197
|
// long-lived user's full session history is a candidate. This compound index keeps that
|
|
193
198
|
// lookup an indexed scan with an ordered result instead of a fetch-then-sort in memory.
|
|
194
199
|
await this.sessionsCollection.createIndex({ userId: 1, lastActive: -1 });
|
|
195
|
-
await this.signupsCollection.createIndex({ email: 1 }, { unique: true });
|
|
200
|
+
await this.signupsCollection.createIndex({ email: 1 }, { unique: true }).catch(() => { });
|
|
196
201
|
await this.signupsCollection.createIndex({ timestamp: -1 });
|
|
197
202
|
await this.salesCollection.createIndex({ email: 1 });
|
|
198
203
|
await this.salesCollection.createIndex({ timestamp: -1 });
|
|
199
|
-
await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true });
|
|
204
|
+
await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
|
|
200
205
|
// TTL index — createIndex is a no-op if the index already exists with the same options.
|
|
201
206
|
// Avoid drop+recreate: Cosmos DB PITR-restored accounts block index drops on restored collections.
|
|
202
207
|
await this.accessTokensCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
203
|
-
await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true });
|
|
208
|
+
await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
|
|
204
209
|
// Issue #563 — org artifacts are unique per (orgId, relativePath); audit reads newest-first.
|
|
205
210
|
// Index creation must never crash startup: Azure Cosmos DB (Mongo API) can
|
|
206
211
|
// reject a unique compound index, and connect() re-throws on failure. Logical
|
|
@@ -224,10 +229,10 @@ class FraimDbService {
|
|
|
224
229
|
await this.pendingVerificationsCollection.createIndex({ email: 1, codeHash: 1 });
|
|
225
230
|
// TTL index — same pattern: create is idempotent, drop is not safe on restored accounts.
|
|
226
231
|
await this.pendingVerificationsCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
227
|
-
await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true });
|
|
232
|
+
await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true }).catch(() => { });
|
|
228
233
|
await this.webhookEventsCollection.createIndex({ type: 1 });
|
|
229
234
|
await this.webhookEventsCollection.createIndex({ receivedAt: -1 });
|
|
230
|
-
await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true });
|
|
235
|
+
await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true }).catch(() => { });
|
|
231
236
|
await this.partnerDiscountsCollection.createIndex({ active: 1 });
|
|
232
237
|
try {
|
|
233
238
|
await this.managerTeamsCollection.createIndex({ managerId: 1, memberId: 1 }, { unique: true });
|
|
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
49
49
|
for (const line of fm[1].split(/\r?\n/)) {
|
|
50
50
|
const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
|
|
51
51
|
if (!match) {
|
|
52
|
-
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|
|
|
52
|
+
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
|
|
53
53
|
malformedField = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
76
76
|
return invalid('Unsupported metadata key spelling');
|
|
77
77
|
if (duplicates.size)
|
|
78
78
|
return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
|
|
79
|
-
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', '
|
|
79
|
+
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
|
|
80
80
|
if (!Object.prototype.hasOwnProperty.call(fields, field))
|
|
81
81
|
return invalid(`Missing metadata: ${field}`);
|
|
82
82
|
}
|
|
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
110
110
|
return invalid('Source is outside its configured learning location');
|
|
111
111
|
if (state === 'archived' && !fields.synthesized)
|
|
112
112
|
return invalid('Archive has no successful processing date');
|
|
113
|
-
const reference = fields.usageSourceRef;
|
|
114
113
|
const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
|
|
115
114
|
if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
|
|
116
115
|
return invalid('Source escapes its owning root');
|
|
117
|
-
if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
|
|
118
|
-
return invalid('Invalid usageSourceRef');
|
|
119
116
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
|
|
120
117
|
return invalid('Sources require an immutable sourceId UUID');
|
|
121
118
|
}
|
|
122
|
-
if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
|
|
123
|
-
|| reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
|
|
124
|
-
return invalid('usageSourceRef must name the owner-relative pending path');
|
|
125
|
-
}
|
|
126
119
|
return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
|
|
127
|
-
date: fields.date, job: fields.job, runId: fields.runId,
|
|
120
|
+
date: fields.date, job: fields.job, runId: fields.runId,
|
|
128
121
|
usageIdentity: `learning-source:${fields.sourceId}`,
|
|
129
122
|
sourceId: fields.sourceId,
|
|
130
123
|
synthesized: fields.synthesized, pendingPath, archivePath, state } };
|