@friggframework/core 2.0.0-next.96 → 2.0.0-next.98

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.
@@ -315,13 +315,13 @@ class OAuth2Requester extends Requester {
315
315
  console.log('[Frigg] Token refresh succeeded');
316
316
  return true;
317
317
  } catch (error) {
318
- console.error('[Frigg] Token refresh failed', {
318
+ const moduleName = this.delegate?.name ?? 'unknown module';
319
+ console.error(`[Frigg] Token refresh failed for ${moduleName}`, {
319
320
  error_message: error?.message,
320
321
  error_name: error?.name,
321
322
  response_status: error?.response?.status,
322
323
  response_data: error?.response?.data,
323
324
  });
324
- await this.notify(this.DLGT_INVALID_AUTH);
325
325
  return false;
326
326
  }
327
327
  }
@@ -119,9 +119,7 @@ class Requester extends Delegate {
119
119
  if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
120
120
  clearRequestTimer();
121
121
  const delay = this.backOff[i] * 1000;
122
- await new Promise((resolve) =>
123
- setTimeout(resolve, delay)
124
- );
122
+ await new Promise((resolve) => setTimeout(resolve, delay));
125
123
  return this._request(url, options, i + 1);
126
124
  }
127
125
  const fetchError = await FetchError.create({
@@ -150,16 +148,24 @@ class Requester extends Delegate {
150
148
  const delay = this.backOff[i] * 1000;
151
149
  await new Promise((resolve) => setTimeout(resolve, delay));
152
150
  return this._request(url, options, i + 1);
153
- } else if (status === 401) {
151
+ }
152
+
153
+ if (status === 401) {
154
154
  if (!this.isRefreshable) {
155
155
  await this.notify(this.DLGT_INVALID_AUTH);
156
- } else if (this.refreshCount === 0) {
156
+ return;
157
+ }
158
+
159
+ if (this.refreshCount === 0) {
157
160
  this.refreshCount++;
158
161
  const refreshSucceeded = await this.refreshAuth();
159
162
  if (refreshSucceeded) {
160
163
  clearRequestTimer();
161
164
  return this._request(url, options, i + 1);
162
165
  }
166
+
167
+ await this.notify(this.DLGT_INVALID_AUTH);
168
+ return;
163
169
  }
164
170
  }
165
171
 
@@ -201,8 +207,7 @@ class Requester extends Delegate {
201
207
  _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
202
208
  if (!err || typeof err !== 'object') return err;
203
209
  if (err.isTimeout) return err;
204
- const isAbort =
205
- err.name === 'AbortError' || err.type === 'aborted';
210
+ const isAbort = err.name === 'AbortError' || err.type === 'aborted';
206
211
  if (!isAbort) return err;
207
212
  err.isTimeout = true;
208
213
  err.timeoutMs = timeoutMs;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.96",
4
+ "version": "2.0.0-next.98",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.96",
42
- "@friggframework/prettier-config": "2.0.0-next.96",
43
- "@friggframework/test": "2.0.0-next.96",
41
+ "@friggframework/eslint-config": "2.0.0-next.98",
42
+ "@friggframework/prettier-config": "2.0.0-next.98",
43
+ "@friggframework/test": "2.0.0-next.98",
44
44
  "@prisma/client": "^6.19.3",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "f69e2249ecdd7a00cfac6ed479b5afab6657bf16"
83
+ "gitHead": "da2c1977216b9f939121b18d8461cc4e8a5b2b94"
84
84
  }
@@ -0,0 +1,86 @@
1
+ # Reporting
2
+
3
+ Read-only, deployment-wide reporting endpoints for `@friggframework/core`.
4
+ Gated by a dedicated reporting API key — **not** the per-user auth used by the
5
+ rest of the Management API.
6
+
7
+ ## Auth
8
+
9
+ Send the reporting key in the `x-frigg-reporting-api-key` header. It is validated
10
+ against the `REPORTING_API_KEY` environment variable. Missing or wrong key → `401`.
11
+
12
+ ## Endpoints
13
+
14
+ | Method | Path | Description |
15
+ | --- | --- | --- |
16
+ | `GET` | `/api/v2/reports` | Index of available reports. |
17
+ | `GET` | `/api/v2/reports/integrations` | Integrations report (see below). |
18
+
19
+ ### `GET /api/v2/reports/integrations`
20
+
21
+ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
22
+ (`config.type` slug), `userId`.
23
+
24
+ ## Response shape
25
+
26
+ ```jsonc
27
+ {
28
+ "schemaVersion": 1,
29
+ "service": "frigg-core-api",
30
+ "generatedAt": "2026-06-29T20:45:05.142Z",
31
+ "filters": { "status": null, "type": null, "userId": null },
32
+ "metrics": {
33
+ "total": 12,
34
+ "byStatus": { "ENABLED": 9, "ERROR": 2, "NEEDS_CONFIG": 1, "PROCESSING": 0, "DISABLED": 0 },
35
+ "byType": [
36
+ {
37
+ "type": "hubspot",
38
+ "label": "HubSpot CRM",
39
+ "total": 5,
40
+ "byStatus": { "ENABLED": 4, "ERROR": 1, "NEEDS_CONFIG": 0, "PROCESSING": 0, "DISABLED": 0 }
41
+ }
42
+ ],
43
+ "typeLabels": { "hubspot": "HubSpot CRM" },
44
+ "integrations": [ /* lightweight per-integration rows */ ]
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Field reference
50
+
51
+ - `schemaVersion` — contract version; branch on it. Additive changes do **not** bump it.
52
+ - `filters` — echoes the applied filters (nulls when omitted).
53
+ - `metrics.total` — count of integrations matching the filters.
54
+ - `metrics.byStatus` — counts keyed by `IntegrationStatus` value.
55
+ - `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus }`.
56
+ - `type` — the `config.type` slug. Integrations with no type bucket as `"unknown"`.
57
+ - `label` — human-readable name from the integration class's
58
+ `Definition.display.label`. Falls back to the `type` slug when no registered
59
+ class supplies a label (e.g. an integration that was removed, or run on an
60
+ older app that doesn't register it). Additive — `type` is unchanged.
61
+ - `metrics.typeLabels` — map of `type` slug → human-readable label, so callers can
62
+ label `integrations[].type` rows without bloating each row. Contains only types
63
+ whose registered class supplies a non-default `display.label` (classes still
64
+ carrying the IntegrationBase default `'Integration Name'` are excluded).
65
+ - `metrics.integrations[]` — lightweight per-integration rows (`id`, `type`,
66
+ `status`, `userId`, `version`, `moduleCount`, `errorCount`, `mappedRecordCount`,
67
+ `createdAt`, `updatedAt`). These rows carry `type` only — resolve display names
68
+ via `metrics.typeLabels`.
69
+
70
+ ## How labels are sourced
71
+
72
+ `createReportingRouter()` loads the app's registered integration classes via
73
+ `loadAppDefinition()` and builds a `{ slug → label }` map from each class's
74
+ `Definition.display.label`. Loading is wrapped in try/catch, so reporting still
75
+ works (labels fall back to slugs) if the app definition can't be loaded. The
76
+ `ListIntegrationsReport` use case stays storage-agnostic — it just reads the
77
+ injected `typeLabels` map, so all database adapters (PostgreSQL, MongoDB,
78
+ DocumentDB) get labels with zero adapter changes.
79
+
80
+ ## Caveats
81
+
82
+ - The response is sensitive (exposes integration types, counts, user IDs, versions).
83
+ Treat the reporting key as an admin secret.
84
+ - Read-only — no mutation endpoints.
85
+ - Labels only appear once the deployment runs a `@friggframework/core` version that
86
+ includes this field and the app registers the integration classes.
@@ -4,11 +4,40 @@ const {
4
4
  createReportingRepository,
5
5
  } = require('./repositories/reporting-repository-factory');
6
6
  const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
7
+ const { loadAppDefinition } = require('../handlers/app-definition-loader');
8
+
9
+ // IntegrationBase.Definition default — skip it so the slug is used instead.
10
+ const PLACEHOLDER_DISPLAY_NAME = 'Integration Name';
11
+
12
+ // Map each integration's config.type slug to its human-readable display label.
13
+ // Wrapped so reporting still works if the app definition fails to load.
14
+ function buildTypeLabels() {
15
+ try {
16
+ const { integrations = [] } = loadAppDefinition();
17
+ const labels = {};
18
+ for (const IntegrationClass of integrations) {
19
+ const def = IntegrationClass?.Definition;
20
+ if (!def?.name) continue;
21
+ const label = def.display?.label;
22
+ if (label && label !== PLACEHOLDER_DISPLAY_NAME) {
23
+ labels[def.name] = label;
24
+ }
25
+ }
26
+ return labels;
27
+ } catch (error) {
28
+ console.error(
29
+ 'Reporting: failed to load integration labels:',
30
+ error.message
31
+ );
32
+ return {};
33
+ }
34
+ }
7
35
 
8
36
  function createReportingRouter() {
9
37
  const reportingRepository = createReportingRepository();
10
38
  const listIntegrationsReport = new ListIntegrationsReport({
11
39
  reportingRepository,
40
+ typeLabels: buildTypeLabels(),
12
41
  });
13
42
 
14
43
  const router = express.Router();
@@ -14,11 +14,12 @@ const KNOWN_STATUSES = [
14
14
  ];
15
15
 
16
16
  class ListIntegrationsReport {
17
- constructor({ reportingRepository } = {}) {
17
+ constructor({ reportingRepository, typeLabels = {} } = {}) {
18
18
  if (!reportingRepository) {
19
19
  throw new Error('reportingRepository is required');
20
20
  }
21
21
  this.reportingRepository = reportingRepository;
22
+ this.typeLabels = typeLabels;
22
23
  }
23
24
 
24
25
  async execute(query = {}) {
@@ -64,6 +65,7 @@ class ListIntegrationsReport {
64
65
  if (!byTypeMap.has(integration.type)) {
65
66
  byTypeMap.set(integration.type, {
66
67
  type: integration.type,
68
+ label: this.typeLabels[integration.type] || integration.type,
67
69
  total: 0,
68
70
  byStatus: emptyStatusCounts(),
69
71
  });
@@ -86,6 +88,7 @@ class ListIntegrationsReport {
86
88
  total: integrations.length,
87
89
  byStatus,
88
90
  byType: Array.from(byTypeMap.values()),
91
+ typeLabels: { ...this.typeLabels },
89
92
  integrations,
90
93
  },
91
94
  };