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.
@@ -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 locator or
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, parsed.record.usageSourceRef,
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|usageSourceRef|synthesized)["']?\s*:/.test(line))
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', 'usageSourceRef', 'synthesized']) {
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, usageSourceRef: fields.usageSourceRef,
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 || 'https://fraim.wellnessatwork.me';
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
- await this.keysCollection.createIndex({ key: 1 }, { unique: true });
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|usageSourceRef|synthesized)["']?\s*:/.test(line))
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', 'usageSourceRef', 'synthesized']) {
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, usageSourceRef: fields.usageSourceRef,
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 } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.357-next.1",
3
+ "version": "2.0.357-next.3",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {