@commonpub/server 2.80.0 → 2.82.0

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,51 @@
1
+ /**
2
+ * CORS origin-pattern matching for the public API.
3
+ *
4
+ * The public API authenticates with `Authorization: Bearer <token>`, NOT
5
+ * cookies. There are no ambient credentials, and we never send
6
+ * `Access-Control-Allow-Credentials: true`. That makes
7
+ * `Access-Control-Allow-Origin: *` SAFE here: a cross-origin page still
8
+ * cannot obtain a key it does not already possess, so reflecting (or
9
+ * wildcarding) the origin only ENABLES legitimate browser clients. The
10
+ * Bearer token is what protects the data.
11
+ *
12
+ * Pattern grammar (the only metacharacter is `*`):
13
+ * * any origin (wildcard-all)
14
+ * localhost shorthand, expands to http(s)://localhost on any port
15
+ * https://app.example.com exact origin
16
+ * http://localhost:* any port on a host
17
+ * https://*.example.com any subdomain
18
+ * *://localhost:* any scheme + any port
19
+ */
20
+ export interface CorsDecision {
21
+ /** Whether the request's origin is permitted. */
22
+ allowed: boolean;
23
+ /** Value for `Access-Control-Allow-Origin`, or null when not allowed. */
24
+ headerValue: string | null;
25
+ /** True when `headerValue` is the literal `*` (cacheable, no `Vary` needed). */
26
+ wildcard: boolean;
27
+ }
28
+ /**
29
+ * True when `origin` is a syntactically valid, reflectable web origin. Used as
30
+ * a gate before reflecting an Origin header — both here (the actual request)
31
+ * and in the preflight echo, which is unauthenticated.
32
+ */
33
+ export declare function isWellFormedOrigin(origin: string): boolean;
34
+ /**
35
+ * Expand shorthand patterns. `localhost` (case-insensitive) becomes both http
36
+ * and https on any port; everything else passes through unchanged (including
37
+ * the bare `*`). Trims each entry and drops empties so a stray comma can't
38
+ * widen the policy.
39
+ */
40
+ export declare function expandOriginPatterns(patterns: readonly string[]): string[];
41
+ /**
42
+ * Decide CORS for an incoming `Origin` against a key's allow-list. Returns a
43
+ * literal `*` when the list permits all origins; otherwise reflects the exact
44
+ * origin (the caller then adds `Vary: Origin`). Returns a deny decision when
45
+ * the list is empty, the origin is missing or malformed, or no pattern matches.
46
+ *
47
+ * The returned shape intentionally has no "credentials" concept — the public
48
+ * API must never pair these headers with `Access-Control-Allow-Credentials`.
49
+ */
50
+ export declare function matchOrigin(patterns: readonly string[] | null | undefined, origin: string | null | undefined): CorsDecision;
51
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/publicApi/cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAkBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAO1D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAqB1E;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,YAAY,CAiBd"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * CORS origin-pattern matching for the public API.
3
+ *
4
+ * The public API authenticates with `Authorization: Bearer <token>`, NOT
5
+ * cookies. There are no ambient credentials, and we never send
6
+ * `Access-Control-Allow-Credentials: true`. That makes
7
+ * `Access-Control-Allow-Origin: *` SAFE here: a cross-origin page still
8
+ * cannot obtain a key it does not already possess, so reflecting (or
9
+ * wildcarding) the origin only ENABLES legitimate browser clients. The
10
+ * Bearer token is what protects the data.
11
+ *
12
+ * Pattern grammar (the only metacharacter is `*`):
13
+ * * any origin (wildcard-all)
14
+ * localhost shorthand, expands to http(s)://localhost on any port
15
+ * https://app.example.com exact origin
16
+ * http://localhost:* any port on a host
17
+ * https://*.example.com any subdomain
18
+ * *://localhost:* any scheme + any port
19
+ */
20
+ const DENY = { allowed: false, headerValue: null, wildcard: false };
21
+ // A syntactically valid web origin: scheme://host[:port], with no path, query,
22
+ // fragment, credentials, whitespace, or control characters. The INCOMING
23
+ // Origin header is validated against this before it is ever reflected into an
24
+ // `Access-Control-Allow-Origin` response header — reflecting an unvalidated
25
+ // header value is a CRLF / response-splitting sink. We validate the value's
26
+ // domain (is this actually an origin?), not just its shape. IPv6-literal and
27
+ // `null` origins are intentionally unsupported: they can't match a pattern
28
+ // anyway, and bracketed/`null` hosts are not worth the reflection risk.
29
+ const WELL_FORMED_ORIGIN = /^[a-z][a-z0-9+.-]*:\/\/[a-z0-9.-]+(?::\d{1,5})?$/i;
30
+ // Control characters (C0 range + DEL), including CR, LF, and TAB. The control
31
+ // characters in this class are the whole point of the check (reject them).
32
+ // eslint-disable-next-line no-control-regex
33
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
34
+ /**
35
+ * True when `origin` is a syntactically valid, reflectable web origin. Used as
36
+ * a gate before reflecting an Origin header — both here (the actual request)
37
+ * and in the preflight echo, which is unauthenticated.
38
+ */
39
+ export function isWellFormedOrigin(origin) {
40
+ if (origin.length > 2000)
41
+ return false;
42
+ // Reject every control character explicitly: a bare `$` (no `m` flag) matches
43
+ // just before a trailing newline, so an anchored test alone would let
44
+ // `https://app.example.com\n` slip through and be reflected into a header.
45
+ if (CONTROL_CHARS.test(origin))
46
+ return false;
47
+ return WELL_FORMED_ORIGIN.test(origin);
48
+ }
49
+ /**
50
+ * Expand shorthand patterns. `localhost` (case-insensitive) becomes both http
51
+ * and https on any port; everything else passes through unchanged (including
52
+ * the bare `*`). Trims each entry and drops empties so a stray comma can't
53
+ * widen the policy.
54
+ */
55
+ export function expandOriginPatterns(patterns) {
56
+ const out = [];
57
+ for (const raw of patterns) {
58
+ const p = raw.trim();
59
+ if (p === '')
60
+ continue;
61
+ if (p.toLowerCase() === 'localhost') {
62
+ // Cover both the default-port origin (`http://localhost`, no colon) and
63
+ // any explicit port (`http://localhost:5173`), over http and https. The
64
+ // validator accepts `localhost` case-insensitively, so normalize here
65
+ // too — otherwise `LOCALHOST` would compile to a dead literal pattern.
66
+ out.push('http://localhost', 'http://localhost:*', 'https://localhost', 'https://localhost:*');
67
+ }
68
+ else {
69
+ out.push(p);
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ // Compiled-pattern cache. Pattern sets are tiny (<=50 per key) and the
75
+ // compiled regex uses only `[^/\s]*` (no nested quantifiers), so there is no
76
+ // ReDoS surface — the cache just avoids recompiling on every request.
77
+ const compiledCache = new Map();
78
+ function compilePattern(pattern) {
79
+ const cached = compiledCache.get(pattern);
80
+ if (cached)
81
+ return cached;
82
+ // Escape every regex metacharacter, THEN turn the escaped `*` back into
83
+ // `[^/\s]*`. Excluding `/` keeps a subdomain wildcard from crossing into a
84
+ // path, and excluding `\s` keeps it from matching newlines/tabs (defense in
85
+ // depth behind `isWellFormedOrigin`). Every `.` stays literal.
86
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
87
+ const withWildcard = escaped.replace(/\\\*/g, '[^/\\s]*');
88
+ const re = new RegExp(`^${withWildcard}$`, 'i');
89
+ compiledCache.set(pattern, re);
90
+ return re;
91
+ }
92
+ /**
93
+ * Decide CORS for an incoming `Origin` against a key's allow-list. Returns a
94
+ * literal `*` when the list permits all origins; otherwise reflects the exact
95
+ * origin (the caller then adds `Vary: Origin`). Returns a deny decision when
96
+ * the list is empty, the origin is missing or malformed, or no pattern matches.
97
+ *
98
+ * The returned shape intentionally has no "credentials" concept — the public
99
+ * API must never pair these headers with `Access-Control-Allow-Credentials`.
100
+ */
101
+ export function matchOrigin(patterns, origin) {
102
+ if (!patterns || patterns.length === 0)
103
+ return DENY;
104
+ const expanded = expandOriginPatterns(patterns);
105
+ if (expanded.includes('*')) {
106
+ // Wildcard-all responds with the literal `*` and never reflects the raw
107
+ // header, so a malformed origin here is harmless.
108
+ return { allowed: true, headerValue: '*', wildcard: true };
109
+ }
110
+ // Reflecting path: the origin will be echoed verbatim into a response header,
111
+ // so it MUST be a well-formed origin first (no CRLF / header injection).
112
+ if (!origin || !isWellFormedOrigin(origin))
113
+ return DENY;
114
+ for (const p of expanded) {
115
+ if (compilePattern(p).test(origin)) {
116
+ return { allowed: true, headerValue: origin, wildcard: false };
117
+ }
118
+ }
119
+ return DENY;
120
+ }
121
+ //# sourceMappingURL=cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.js","sourceRoot":"","sources":["../../src/publicApi/cors.ts"],"names":[],"mappings":"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"}
@@ -4,12 +4,18 @@ export type { GeneratedKey } from './keys.js';
4
4
  export type { ApiKey } from '@commonpub/schema';
5
5
  export { apiKeyRateLimit, ApiKeyRateLimit } from './rateLimit.js';
6
6
  export type { RateLimitResult } from './rateLimit.js';
7
+ export { matchOrigin, expandOriginPatterns, isWellFormedOrigin } from './cors.js';
8
+ export type { CorsDecision } from './cors.js';
7
9
  export { authenticateApiKey } from './auth.js';
8
10
  export type { AuthResult, AuthSuccess, AuthRejected, AuthFailure } from './auth.js';
9
11
  export { createApiKey, listApiKeys, revokeApiKey, getApiKeyById, logApiKeyUsage, touchLastUsed, } from './adminOps.js';
10
12
  export type { CreateApiKeyResult } from './adminOps.js';
11
13
  export { getApiKeyUsageStats } from './usage.js';
12
14
  export type { ApiKeyUsageStats } from './usage.js';
15
+ export { METRICS_MIN_BUCKET, getMetricsOverview, getTopContent, getTrendingTags, getTopContributors, getEngagementMetrics, getFederationReach, } from './metrics.js';
16
+ export type { MetricsOverview, ContentMetric, MetricsTopContributor, MetricsEngagement, MetricsFederationReach, } from './metrics.js';
17
+ export { TIMESERIES_METRICS, runDailyRollup, backfillMetricsDaily, getMetricsTimeseries, } from './metricsRollup.js';
18
+ export type { MetricKind, TimeseriesInterval, TimeseriesPoint, MetricsTimeseries, } from './metricsRollup.js';
13
19
  export { toPublicUser, isPublicUser, toPublicContentSummary, toPublicContentDetail, isPublicContent, toPublicHub, isPublicHub, toAdminApiKeyView, toPublicLearningPath, isPublicLearningPath, toPublicEvent, isPublicEvent, toPublicContest, isPublicContest, toPublicVideo, isPublicVideo, toPublicDocSite, isPublicDocSite, toPublicTag, } from './serializers.js';
14
20
  export type { PublicUser, PublicUserRow, PublicContentSummary, PublicContentDetail, PublicContentRow, PublicHub, PublicHubRow, PublicInstance, AdminApiKeyView, PublicLearningPath, PublicLearningPathRow, PublicEvent, PublicEventRow, PublicContest, PublicContestRow, PublicVideo, PublicVideoRow, PublicDocSite, PublicDocSiteRow, PublicTag, PublicTagRow, } from './serializers.js';
15
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/publicApi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACtF,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,YAAY,GACb,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/publicApi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACtF,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAClF,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,YAAY,GACb,MAAM,kBAAkB,CAAC"}
@@ -1,8 +1,11 @@
1
1
  export { hasScope, filterKnownScopes } from './scopes.js';
2
2
  export { generateApiKey, hashApiKey, compareKeyHash, extractPrefix } from './keys.js';
3
3
  export { apiKeyRateLimit, ApiKeyRateLimit } from './rateLimit.js';
4
+ export { matchOrigin, expandOriginPatterns, isWellFormedOrigin } from './cors.js';
4
5
  export { authenticateApiKey } from './auth.js';
5
6
  export { createApiKey, listApiKeys, revokeApiKey, getApiKeyById, logApiKeyUsage, touchLastUsed, } from './adminOps.js';
6
7
  export { getApiKeyUsageStats } from './usage.js';
8
+ export { METRICS_MIN_BUCKET, getMetricsOverview, getTopContent, getTrendingTags, getTopContributors, getEngagementMetrics, getFederationReach, } from './metrics.js';
9
+ export { TIMESERIES_METRICS, runDailyRollup, backfillMetricsDaily, getMetricsTimeseries, } from './metricsRollup.js';
7
10
  export { toPublicUser, isPublicUser, toPublicContentSummary, toPublicContentDetail, isPublicContent, toPublicHub, isPublicHub, toAdminApiKeyView, toPublicLearningPath, isPublicLearningPath, toPublicEvent, isPublicEvent, toPublicContest, isPublicContest, toPublicVideo, isPublicVideo, toPublicDocSite, isPublicDocSite, toPublicTag, } from './serializers.js';
8
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/publicApi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGtF,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/publicApi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGtF,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAElF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAQtB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,117 @@
1
+ import type { DB } from '../types.js';
2
+ import { type PublicContentSummary, type PublicTag } from './serializers.js';
3
+ /**
4
+ * DevRel / company analytics metrics for the public API. Phase 2: instantaneous
5
+ * aggregates read straight from denormalized counters and timestamps — no
6
+ * per-event tracking, no new PII. Time-series (Phase 3) comes from daily rollups.
7
+ *
8
+ * Privacy contract (enforced here):
9
+ * - Aggregates and intentional public leaderboards only; never per-user activity.
10
+ * - Only published + public + non-deleted content and public-profile active users
11
+ * are counted, at the SQL WHERE level (so the indexes are actually used).
12
+ * - No IP / user-agent / email / referrer is read or returned.
13
+ * - k-anonymity (`METRICS_MIN_BUCKET`) guards any future user-pivotable breakdown
14
+ * (Phase 3 rollups). Phase 2 exposes only non-pivotable aggregates and the
15
+ * contributor leaderboard, which is intentional public attribution.
16
+ */
17
+ /** Suppression threshold for dimensioned breakdowns over users (Phase 3 use). */
18
+ export declare const METRICS_MIN_BUCKET = 5;
19
+ export interface MetricsOverview {
20
+ domain: string;
21
+ generatedAt: string;
22
+ totals: {
23
+ users: number;
24
+ contributors: number;
25
+ content: {
26
+ total: number;
27
+ byType: Record<string, number>;
28
+ };
29
+ hubs: number;
30
+ tags: number;
31
+ engagement: {
32
+ views: number;
33
+ likes: number;
34
+ comments: number;
35
+ };
36
+ };
37
+ recent: {
38
+ newUsers: {
39
+ last7d: number;
40
+ last30d: number;
41
+ };
42
+ newContent: {
43
+ last7d: number;
44
+ last30d: number;
45
+ };
46
+ activeContributors: {
47
+ last7d: number;
48
+ last30d: number;
49
+ };
50
+ };
51
+ notes: string[];
52
+ }
53
+ export declare function getMetricsOverview(db: DB, domain: string): Promise<MetricsOverview>;
54
+ export type ContentMetric = 'views' | 'likes' | 'comments';
55
+ export declare function getTopContent(db: DB, domain: string, opts: {
56
+ metric: ContentMetric;
57
+ type?: 'project' | 'article' | 'blog' | 'explainer';
58
+ limit: number;
59
+ }): Promise<PublicContentSummary[]>;
60
+ export declare function getTrendingTags(db: DB, domain: string, limit: number): Promise<PublicTag[]>;
61
+ export interface MetricsTopContributor {
62
+ user: {
63
+ id: string;
64
+ username: string;
65
+ displayName: string | null;
66
+ avatarUrl: string | null;
67
+ };
68
+ publishedContent: number;
69
+ totalViews: number;
70
+ totalLikes: number;
71
+ canonicalUrl: string;
72
+ }
73
+ export declare function getTopContributors(db: DB, domain: string, limit: number): Promise<MetricsTopContributor[]>;
74
+ export interface MetricsEngagement {
75
+ content: {
76
+ published: number;
77
+ views: number;
78
+ likes: number;
79
+ comments: number;
80
+ avgViewsPerItem: number;
81
+ likesPerView: number;
82
+ commentsPerView: number;
83
+ };
84
+ learning?: {
85
+ paths: number;
86
+ enrollments: number;
87
+ completions: number;
88
+ completionRate: number;
89
+ };
90
+ events?: {
91
+ events: number;
92
+ capacity: number;
93
+ attendees: number;
94
+ fillRate: number;
95
+ };
96
+ contests?: {
97
+ contests: number;
98
+ entries: number;
99
+ };
100
+ }
101
+ export declare function getEngagementMetrics(db: DB, features: {
102
+ learning?: boolean;
103
+ events?: boolean;
104
+ contests?: boolean;
105
+ }): Promise<MetricsEngagement>;
106
+ export interface MetricsFederationReach {
107
+ knownInstances: number;
108
+ activeMirrors: number;
109
+ followers: number;
110
+ inboundContent: number;
111
+ inboundByDomain: Array<{
112
+ domain: string;
113
+ count: number;
114
+ }>;
115
+ }
116
+ export declare function getFederationReach(db: DB, limit: number): Promise<MetricsFederationReach>;
117
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/publicApi/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,317 @@
1
+ import { contentItems, users, hubs, tags, learningPaths, events, contests, federatedContent, instanceMirrors, registryInstances, followRelationships, hubFollowers, } from '@commonpub/schema';
2
+ import { and, desc, eq, gt, inArray, isNull, sql } from 'drizzle-orm';
3
+ import { toPublicContentSummary, toPublicTag, } from './serializers.js';
4
+ /**
5
+ * DevRel / company analytics metrics for the public API. Phase 2: instantaneous
6
+ * aggregates read straight from denormalized counters and timestamps — no
7
+ * per-event tracking, no new PII. Time-series (Phase 3) comes from daily rollups.
8
+ *
9
+ * Privacy contract (enforced here):
10
+ * - Aggregates and intentional public leaderboards only; never per-user activity.
11
+ * - Only published + public + non-deleted content and public-profile active users
12
+ * are counted, at the SQL WHERE level (so the indexes are actually used).
13
+ * - No IP / user-agent / email / referrer is read or returned.
14
+ * - k-anonymity (`METRICS_MIN_BUCKET`) guards any future user-pivotable breakdown
15
+ * (Phase 3 rollups). Phase 2 exposes only non-pivotable aggregates and the
16
+ * contributor leaderboard, which is intentional public attribution.
17
+ */
18
+ /** Suppression threshold for dimensioned breakdowns over users (Phase 3 use). */
19
+ export const METRICS_MIN_BUCKET = 5;
20
+ const DAY_MS = 86_400_000;
21
+ // Counter SUMs cast to float8, not int4: a cumulative `sum(view_count)` can
22
+ // exceed int4's 2.1B ceiling on a busy instance, where `::int` would throw
23
+ // "integer out of range" and 500 the endpoint. float8 holds integer sums
24
+ // exactly up to 2^53 and the pg driver returns it as a JS number (no string
25
+ // coercion). count(*) / count(distinct) stay ::int — row counts won't overflow.
26
+ /** Published + public + non-deleted content — the only content any metric counts. */
27
+ function publicContentWhere() {
28
+ return and(eq(contentItems.status, 'published'), eq(contentItems.visibility, 'public'), isNull(contentItems.deletedAt));
29
+ }
30
+ // Only DB-valid, publicly-visible statuses. The events status enum is
31
+ // active|draft|published|completed|cancelled (no 'upcoming'/'past' — those are
32
+ // presentation-only labels), so the public set is published/active/completed.
33
+ const PUBLIC_EVENT_STATUSES = [
34
+ 'published',
35
+ 'active',
36
+ 'completed',
37
+ ];
38
+ const PUBLIC_CONTEST_STATUSES = [
39
+ 'upcoming',
40
+ 'active',
41
+ 'judging',
42
+ 'completed',
43
+ ];
44
+ export async function getMetricsOverview(db, domain) {
45
+ const now = Date.now();
46
+ const since7 = new Date(now - 7 * DAY_MS);
47
+ const since30 = new Date(now - 30 * DAY_MS);
48
+ const pub = publicContentWhere();
49
+ const [[contentAgg], byType, [userAgg], [hubAgg], [tagAgg]] = await Promise.all([
50
+ db
51
+ .select({
52
+ total: sql `count(*)::int`,
53
+ views: sql `coalesce(sum(${contentItems.viewCount}), 0)::float8`,
54
+ likes: sql `coalesce(sum(${contentItems.likeCount}), 0)::float8`,
55
+ comments: sql `coalesce(sum(${contentItems.commentCount}), 0)::float8`,
56
+ contributors: sql `count(distinct ${contentItems.authorId})::int`,
57
+ new7: sql `count(*) FILTER (WHERE ${contentItems.publishedAt} > ${since7})::int`,
58
+ new30: sql `count(*) FILTER (WHERE ${contentItems.publishedAt} > ${since30})::int`,
59
+ active7: sql `count(distinct ${contentItems.authorId}) FILTER (WHERE ${contentItems.publishedAt} > ${since7})::int`,
60
+ active30: sql `count(distinct ${contentItems.authorId}) FILTER (WHERE ${contentItems.publishedAt} > ${since30})::int`,
61
+ })
62
+ .from(contentItems)
63
+ .where(pub),
64
+ db
65
+ .select({ type: contentItems.type, count: sql `count(*)::int` })
66
+ .from(contentItems)
67
+ .where(pub)
68
+ .groupBy(contentItems.type),
69
+ db
70
+ .select({
71
+ total: sql `count(*)::int`,
72
+ new7: sql `count(*) FILTER (WHERE ${users.createdAt} > ${since7})::int`,
73
+ new30: sql `count(*) FILTER (WHERE ${users.createdAt} > ${since30})::int`,
74
+ })
75
+ .from(users)
76
+ .where(and(isNull(users.deletedAt), eq(users.status, 'active'))),
77
+ db.select({ total: sql `count(*)::int` }).from(hubs).where(isNull(hubs.deletedAt)),
78
+ db.select({ total: sql `count(*)::int` }).from(tags),
79
+ ]);
80
+ const byTypeRecord = {};
81
+ for (const r of byType)
82
+ byTypeRecord[r.type] = r.count;
83
+ return {
84
+ domain,
85
+ generatedAt: new Date(now).toISOString(),
86
+ totals: {
87
+ users: userAgg?.total ?? 0,
88
+ contributors: contentAgg?.contributors ?? 0,
89
+ content: { total: contentAgg?.total ?? 0, byType: byTypeRecord },
90
+ hubs: hubAgg?.total ?? 0,
91
+ tags: tagAgg?.total ?? 0,
92
+ engagement: {
93
+ views: contentAgg?.views ?? 0,
94
+ likes: contentAgg?.likes ?? 0,
95
+ comments: contentAgg?.comments ?? 0,
96
+ },
97
+ },
98
+ recent: {
99
+ newUsers: { last7d: userAgg?.new7 ?? 0, last30d: userAgg?.new30 ?? 0 },
100
+ newContent: { last7d: contentAgg?.new7 ?? 0, last30d: contentAgg?.new30 ?? 0 },
101
+ activeContributors: { last7d: contentAgg?.active7 ?? 0, last30d: contentAgg?.active30 ?? 0 },
102
+ },
103
+ notes: [
104
+ 'Counts cover published, public, non-deleted content and active public-profile users only.',
105
+ 'Engagement totals are cumulative; per-day engagement time-series arrives with Phase 3 rollups.',
106
+ ],
107
+ };
108
+ }
109
+ export async function getTopContent(db, domain, opts) {
110
+ const orderCol = opts.metric === 'likes'
111
+ ? contentItems.likeCount
112
+ : opts.metric === 'comments'
113
+ ? contentItems.commentCount
114
+ : contentItems.viewCount;
115
+ const where = opts.type
116
+ ? and(publicContentWhere(), eq(contentItems.type, opts.type))
117
+ : publicContentWhere();
118
+ const rows = await db
119
+ .select({
120
+ id: contentItems.id,
121
+ type: contentItems.type,
122
+ title: contentItems.title,
123
+ slug: contentItems.slug,
124
+ description: contentItems.description,
125
+ coverImageUrl: contentItems.coverImageUrl,
126
+ difficulty: contentItems.difficulty,
127
+ status: contentItems.status,
128
+ visibility: contentItems.visibility,
129
+ publishedAt: contentItems.publishedAt,
130
+ updatedAt: contentItems.updatedAt,
131
+ createdAt: contentItems.createdAt,
132
+ viewCount: contentItems.viewCount,
133
+ likeCount: contentItems.likeCount,
134
+ commentCount: contentItems.commentCount,
135
+ authorId: users.id,
136
+ authorUsername: users.username,
137
+ authorDisplayName: users.displayName,
138
+ authorAvatarUrl: users.avatarUrl,
139
+ })
140
+ .from(contentItems)
141
+ .innerJoin(users, eq(contentItems.authorId, users.id))
142
+ // Unique id tiebreaker keeps the ordering deterministic across equal counts.
143
+ .where(where)
144
+ .orderBy(desc(orderCol), desc(contentItems.id))
145
+ .limit(opts.limit);
146
+ return rows.map((r) => toPublicContentSummary({
147
+ id: r.id,
148
+ type: r.type,
149
+ title: r.title,
150
+ slug: r.slug,
151
+ description: r.description,
152
+ coverImageUrl: r.coverImageUrl,
153
+ difficulty: r.difficulty,
154
+ status: r.status,
155
+ visibility: r.visibility,
156
+ publishedAt: r.publishedAt,
157
+ updatedAt: r.updatedAt ?? undefined,
158
+ createdAt: r.createdAt ?? undefined,
159
+ deletedAt: null,
160
+ viewCount: r.viewCount,
161
+ likeCount: r.likeCount,
162
+ commentCount: r.commentCount,
163
+ author: {
164
+ id: r.authorId,
165
+ username: r.authorUsername,
166
+ displayName: r.authorDisplayName,
167
+ avatarUrl: r.authorAvatarUrl,
168
+ },
169
+ }, domain));
170
+ }
171
+ // --- Trending tags ---
172
+ export async function getTrendingTags(db, domain, limit) {
173
+ const rows = await db
174
+ .select({ id: tags.id, name: tags.name, slug: tags.slug, usageCount: tags.usageCount })
175
+ .from(tags)
176
+ .where(gt(tags.usageCount, 0))
177
+ .orderBy(desc(tags.usageCount), desc(tags.id))
178
+ .limit(limit);
179
+ return rows.map((r) => toPublicTag(r, domain));
180
+ }
181
+ export async function getTopContributors(db, domain, limit) {
182
+ const rows = await db
183
+ .select({
184
+ id: users.id,
185
+ username: users.username,
186
+ displayName: users.displayName,
187
+ avatarUrl: users.avatarUrl,
188
+ publishedContent: sql `count(${contentItems.id})::int`,
189
+ totalViews: sql `coalesce(sum(${contentItems.viewCount}), 0)::float8`,
190
+ totalLikes: sql `coalesce(sum(${contentItems.likeCount}), 0)::float8`,
191
+ })
192
+ .from(users)
193
+ // INNER JOIN on the public-content predicate: users with zero public content
194
+ // are excluded (they are not contributors), and only public content counts.
195
+ .innerJoin(contentItems, and(eq(contentItems.authorId, users.id), publicContentWhere()))
196
+ .where(and(eq(users.profileVisibility, 'public'), eq(users.status, 'active'), isNull(users.deletedAt)))
197
+ .groupBy(users.id, users.username, users.displayName, users.avatarUrl)
198
+ .orderBy(desc(sql `count(${contentItems.id})`), desc(users.id))
199
+ .limit(limit);
200
+ return rows.map((r) => ({
201
+ user: { id: r.id, username: r.username, displayName: r.displayName, avatarUrl: r.avatarUrl },
202
+ publishedContent: r.publishedContent,
203
+ totalViews: r.totalViews,
204
+ totalLikes: r.totalLikes,
205
+ canonicalUrl: `https://${domain}/u/${r.username}`,
206
+ }));
207
+ }
208
+ function ratio(numerator, denominator, digits = 3) {
209
+ if (denominator <= 0)
210
+ return 0;
211
+ const factor = 10 ** digits;
212
+ return Math.round((numerator / denominator) * factor) / factor;
213
+ }
214
+ export async function getEngagementMetrics(db, features) {
215
+ const [contentAgg] = await db
216
+ .select({
217
+ published: sql `count(*)::int`,
218
+ views: sql `coalesce(sum(${contentItems.viewCount}), 0)::float8`,
219
+ likes: sql `coalesce(sum(${contentItems.likeCount}), 0)::float8`,
220
+ comments: sql `coalesce(sum(${contentItems.commentCount}), 0)::float8`,
221
+ })
222
+ .from(contentItems)
223
+ .where(publicContentWhere());
224
+ const published = contentAgg?.published ?? 0;
225
+ const views = contentAgg?.views ?? 0;
226
+ const likes = contentAgg?.likes ?? 0;
227
+ const comments = contentAgg?.comments ?? 0;
228
+ const result = {
229
+ content: {
230
+ published,
231
+ views,
232
+ likes,
233
+ comments,
234
+ avgViewsPerItem: ratio(views, published, 2),
235
+ likesPerView: ratio(likes, views),
236
+ commentsPerView: ratio(comments, views),
237
+ },
238
+ };
239
+ if (features.learning) {
240
+ const [l] = await db
241
+ .select({
242
+ paths: sql `count(*)::int`,
243
+ enrollments: sql `coalesce(sum(${learningPaths.enrollmentCount}), 0)::float8`,
244
+ completions: sql `coalesce(sum(${learningPaths.completionCount}), 0)::float8`,
245
+ })
246
+ .from(learningPaths)
247
+ .where(eq(learningPaths.status, 'published'));
248
+ result.learning = {
249
+ paths: l?.paths ?? 0,
250
+ enrollments: l?.enrollments ?? 0,
251
+ completions: l?.completions ?? 0,
252
+ completionRate: ratio(l?.completions ?? 0, l?.enrollments ?? 0),
253
+ };
254
+ }
255
+ if (features.events) {
256
+ const [e] = await db
257
+ .select({
258
+ events: sql `count(*)::int`,
259
+ capacity: sql `coalesce(sum(${events.capacity}), 0)::float8`,
260
+ attendees: sql `coalesce(sum(${events.attendeeCount}), 0)::float8`,
261
+ })
262
+ .from(events)
263
+ .where(inArray(events.status, PUBLIC_EVENT_STATUSES));
264
+ result.events = {
265
+ events: e?.events ?? 0,
266
+ capacity: e?.capacity ?? 0,
267
+ attendees: e?.attendees ?? 0,
268
+ fillRate: ratio(e?.attendees ?? 0, e?.capacity ?? 0),
269
+ };
270
+ }
271
+ if (features.contests) {
272
+ const [c] = await db
273
+ .select({
274
+ contests: sql `count(*)::int`,
275
+ entries: sql `coalesce(sum(${contests.entryCount}), 0)::float8`,
276
+ })
277
+ .from(contests)
278
+ .where(and(inArray(contests.status, PUBLIC_CONTEST_STATUSES), eq(contests.visibility, 'public')));
279
+ result.contests = { contests: c?.contests ?? 0, entries: c?.entries ?? 0 };
280
+ }
281
+ return result;
282
+ }
283
+ export async function getFederationReach(db, limit) {
284
+ const [[instAgg], [mirrorAgg], [userFollowers], [hubFollowerAgg], [inboundAgg], byDomain] = await Promise.all([
285
+ db
286
+ .select({ total: sql `count(*)::int` })
287
+ .from(registryInstances)
288
+ .where(eq(registryInstances.status, 'active')),
289
+ db
290
+ .select({ total: sql `count(*)::int` })
291
+ .from(instanceMirrors)
292
+ .where(eq(instanceMirrors.status, 'active')),
293
+ db
294
+ .select({ total: sql `count(*)::int` })
295
+ .from(followRelationships)
296
+ .where(eq(followRelationships.status, 'accepted')),
297
+ db
298
+ .select({ total: sql `count(*)::int` })
299
+ .from(hubFollowers)
300
+ .where(eq(hubFollowers.status, 'accepted')),
301
+ db.select({ total: sql `count(*)::int` }).from(federatedContent),
302
+ db
303
+ .select({ domain: federatedContent.originDomain, count: sql `count(*)::int` })
304
+ .from(federatedContent)
305
+ .groupBy(federatedContent.originDomain)
306
+ .orderBy(desc(sql `count(*)`), desc(federatedContent.originDomain))
307
+ .limit(limit),
308
+ ]);
309
+ return {
310
+ knownInstances: instAgg?.total ?? 0,
311
+ activeMirrors: mirrorAgg?.total ?? 0,
312
+ followers: (userFollowers?.total ?? 0) + (hubFollowerAgg?.total ?? 0),
313
+ inboundContent: inboundAgg?.total ?? 0,
314
+ inboundByDomain: byDomain.map((r) => ({ domain: r.domain, count: r.count })),
315
+ };
316
+ }
317
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/publicApi/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { DB } from '../types.js';
2
+ /**
3
+ * Daily analytics rollups (Phase 3). Aggregate snapshots only — no per-user
4
+ * data, no new PII. Written by the `metrics-rollup` Nitro plugin into
5
+ * `metrics_daily`; read by `getMetricsTimeseries`.
6
+ *
7
+ * Two metric kinds:
8
+ * - `flow` (e.g. `users.new`): that day's count. Deterministic from timestamps,
9
+ * so fully backfillable and idempotent on re-run.
10
+ * - `cumulative` (e.g. `users.total`, `content.views`): running total as of a
11
+ * day. Count-based cumulatives are backfilled as the survivorship curve
12
+ * (currently-live rows by their creation/publish date). Sum-based engagement
13
+ * cumulatives (views/likes/comments) have no per-day history, so they are
14
+ * snapshot forward from the first rollup only (never backfilled).
15
+ */
16
+ export type MetricKind = 'flow' | 'cumulative';
17
+ /** Every metric the timeseries endpoint will serve, with its aggregation kind. */
18
+ export declare const TIMESERIES_METRICS: Record<string, MetricKind>;
19
+ /**
20
+ * Snapshot today's metrics. Count-based + flow metrics are deterministic
21
+ * (`users.total` = currently-live users; `users.new` = created today). Sum-based
22
+ * engagement cumulatives capture the current running total under `today`.
23
+ * Idempotent: re-running for the same day overwrites that day's rows.
24
+ */
25
+ export declare function runDailyRollup(db: DB, today: string): Promise<void>;
26
+ /**
27
+ * Backfill the deterministic count-based series for ALL of history from
28
+ * timestamps: `users.new`/`content.new` (daily flow) and `users.total`/
29
+ * `content.total` (cumulative window-sum = survivorship curve). Sum-based
30
+ * engagement metrics are NOT backfilled (no per-day history exists). Idempotent.
31
+ * Returns the number of rows written.
32
+ */
33
+ export declare function backfillMetricsDaily(db: DB): Promise<number>;
34
+ export type TimeseriesInterval = 'day' | 'week' | 'month';
35
+ export interface TimeseriesPoint {
36
+ date: string;
37
+ value: number;
38
+ delta: number;
39
+ }
40
+ export interface MetricsTimeseries {
41
+ metric: string;
42
+ kind: MetricKind;
43
+ interval: TimeseriesInterval;
44
+ from: string;
45
+ to: string;
46
+ since: string | null;
47
+ points: TimeseriesPoint[];
48
+ }
49
+ export declare function getMetricsTimeseries(db: DB, opts: {
50
+ metric: string;
51
+ interval: TimeseriesInterval;
52
+ from: string;
53
+ to: string;
54
+ }): Promise<MetricsTimeseries>;
55
+ //# sourceMappingURL=metricsRollup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsRollup.d.ts","sourceRoot":"","sources":["../../src/publicApi/metricsRollup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;AAE/C,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAQzD,CAAC;AA6BF;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BzE;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmClE;AAID,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAaD,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,EAAE,EACN,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC/E,OAAO,CAAC,iBAAiB,CAAC,CA4C5B"}
@@ -0,0 +1,158 @@
1
+ import { metricsDaily, contentItems, users } from '@commonpub/schema';
2
+ import { and, asc, eq, gte, isNull, lte, sql } from 'drizzle-orm';
3
+ /** Every metric the timeseries endpoint will serve, with its aggregation kind. */
4
+ export const TIMESERIES_METRICS = {
5
+ 'users.total': 'cumulative',
6
+ 'users.new': 'flow',
7
+ 'content.total': 'cumulative',
8
+ 'content.new': 'flow',
9
+ 'content.views': 'cumulative',
10
+ 'content.likes': 'cumulative',
11
+ 'content.comments': 'cumulative',
12
+ };
13
+ const DIM = ''; // global series sentinel (see metrics_daily schema note)
14
+ function activeUsersWhere() {
15
+ return and(isNull(users.deletedAt), eq(users.status, 'active'));
16
+ }
17
+ function publicContentWhere() {
18
+ return and(eq(contentItems.status, 'published'), eq(contentItems.visibility, 'public'), isNull(contentItems.deletedAt));
19
+ }
20
+ async function upsertRows(db, rows) {
21
+ if (rows.length === 0)
22
+ return;
23
+ await db
24
+ .insert(metricsDaily)
25
+ .values(rows.map((r) => ({ day: r.day, metric: r.metric, dimension: DIM, value: r.value })))
26
+ .onConflictDoUpdate({
27
+ target: [metricsDaily.day, metricsDaily.metric, metricsDaily.dimension],
28
+ set: { value: sql `excluded.value` },
29
+ });
30
+ }
31
+ /**
32
+ * Snapshot today's metrics. Count-based + flow metrics are deterministic
33
+ * (`users.total` = currently-live users; `users.new` = created today). Sum-based
34
+ * engagement cumulatives capture the current running total under `today`.
35
+ * Idempotent: re-running for the same day overwrites that day's rows.
36
+ */
37
+ export async function runDailyRollup(db, today) {
38
+ const [[u], [c]] = await Promise.all([
39
+ db
40
+ .select({
41
+ total: sql `count(*)::int`,
42
+ newToday: sql `count(*) FILTER (WHERE ${users.createdAt}::date = ${today})::int`,
43
+ })
44
+ .from(users)
45
+ .where(activeUsersWhere()),
46
+ db
47
+ .select({
48
+ total: sql `count(*)::int`,
49
+ newToday: sql `count(*) FILTER (WHERE ${contentItems.publishedAt}::date = ${today})::int`,
50
+ views: sql `coalesce(sum(${contentItems.viewCount}), 0)::float8`,
51
+ likes: sql `coalesce(sum(${contentItems.likeCount}), 0)::float8`,
52
+ comments: sql `coalesce(sum(${contentItems.commentCount}), 0)::float8`,
53
+ })
54
+ .from(contentItems)
55
+ .where(publicContentWhere()),
56
+ ]);
57
+ await upsertRows(db, [
58
+ { day: today, metric: 'users.total', value: u?.total ?? 0 },
59
+ { day: today, metric: 'users.new', value: u?.newToday ?? 0 },
60
+ { day: today, metric: 'content.total', value: c?.total ?? 0 },
61
+ { day: today, metric: 'content.new', value: c?.newToday ?? 0 },
62
+ { day: today, metric: 'content.views', value: c?.views ?? 0 },
63
+ { day: today, metric: 'content.likes', value: c?.likes ?? 0 },
64
+ { day: today, metric: 'content.comments', value: c?.comments ?? 0 },
65
+ ]);
66
+ }
67
+ /**
68
+ * Backfill the deterministic count-based series for ALL of history from
69
+ * timestamps: `users.new`/`content.new` (daily flow) and `users.total`/
70
+ * `content.total` (cumulative window-sum = survivorship curve). Sum-based
71
+ * engagement metrics are NOT backfilled (no per-day history exists). Idempotent.
72
+ * Returns the number of rows written.
73
+ */
74
+ export async function backfillMetricsDaily(db) {
75
+ const [userDays, contentDays] = await Promise.all([
76
+ db
77
+ .select({
78
+ day: sql `to_char(${users.createdAt}::date, 'YYYY-MM-DD')`,
79
+ flow: sql `count(*)::int`,
80
+ cumulative: sql `sum(count(*)) OVER (ORDER BY ${users.createdAt}::date)::int`,
81
+ })
82
+ .from(users)
83
+ .where(activeUsersWhere())
84
+ .groupBy(sql `${users.createdAt}::date`)
85
+ .orderBy(sql `${users.createdAt}::date`),
86
+ db
87
+ .select({
88
+ day: sql `to_char(${contentItems.publishedAt}::date, 'YYYY-MM-DD')`,
89
+ flow: sql `count(*)::int`,
90
+ cumulative: sql `sum(count(*)) OVER (ORDER BY ${contentItems.publishedAt}::date)::int`,
91
+ })
92
+ .from(contentItems)
93
+ .where(and(publicContentWhere(), sql `${contentItems.publishedAt} IS NOT NULL`))
94
+ .groupBy(sql `${contentItems.publishedAt}::date`)
95
+ .orderBy(sql `${contentItems.publishedAt}::date`),
96
+ ]);
97
+ const rows = [];
98
+ for (const r of userDays) {
99
+ rows.push({ day: r.day, metric: 'users.new', value: r.flow });
100
+ rows.push({ day: r.day, metric: 'users.total', value: r.cumulative });
101
+ }
102
+ for (const r of contentDays) {
103
+ rows.push({ day: r.day, metric: 'content.new', value: r.flow });
104
+ rows.push({ day: r.day, metric: 'content.total', value: r.cumulative });
105
+ }
106
+ await upsertRows(db, rows);
107
+ return rows.length;
108
+ }
109
+ /** UTC bucket-start key for a YYYY-MM-DD day under the given interval. */
110
+ function bucketKey(day, interval) {
111
+ if (interval === 'day')
112
+ return day;
113
+ if (interval === 'month')
114
+ return `${day.slice(0, 7)}-01`;
115
+ // week: Monday of the ISO week (UTC).
116
+ const d = new Date(`${day}T00:00:00Z`);
117
+ const dow = (d.getUTCDay() + 6) % 7; // 0 = Monday
118
+ d.setUTCDate(d.getUTCDate() - dow);
119
+ return d.toISOString().slice(0, 10);
120
+ }
121
+ export async function getMetricsTimeseries(db, opts) {
122
+ const kind = TIMESERIES_METRICS[opts.metric];
123
+ if (!kind)
124
+ throw new Error(`Unknown metric: ${opts.metric}`);
125
+ const rows = await db
126
+ .select({ day: metricsDaily.day, value: metricsDaily.value })
127
+ .from(metricsDaily)
128
+ .where(and(eq(metricsDaily.metric, opts.metric), eq(metricsDaily.dimension, DIM), gte(metricsDaily.day, opts.from), lte(metricsDaily.day, opts.to)))
129
+ .orderBy(asc(metricsDaily.day));
130
+ // Bucket in JS (range is bounded by the caller). flow -> sum within bucket;
131
+ // cumulative -> last value within bucket (the running total at bucket end).
132
+ const buckets = new Map();
133
+ for (const r of rows) {
134
+ const key = bucketKey(r.day, opts.interval);
135
+ const prev = buckets.get(key);
136
+ if (kind === 'flow')
137
+ buckets.set(key, (prev ?? 0) + r.value);
138
+ else
139
+ buckets.set(key, r.value); // ordered asc, so last write wins = bucket-end value
140
+ }
141
+ const ordered = [...buckets.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
142
+ const points = [];
143
+ let prevValue = null;
144
+ for (const [date, value] of ordered) {
145
+ points.push({ date, value, delta: prevValue === null ? 0 : value - prevValue });
146
+ prevValue = value;
147
+ }
148
+ return {
149
+ metric: opts.metric,
150
+ kind,
151
+ interval: opts.interval,
152
+ from: opts.from,
153
+ to: opts.to,
154
+ since: rows[0]?.day ?? null,
155
+ points,
156
+ };
157
+ }
158
+ //# sourceMappingURL=metricsRollup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricsRollup.js","sourceRoot":"","sources":["../../src/publicApi/metricsRollup.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/server",
3
- "version": "2.80.0",
3
+ "version": "2.82.0",
4
4
  "type": "module",
5
5
  "description": "Framework-agnostic business logic for CommonPub instances",
6
6
  "license": "AGPL-3.0-or-later",
@@ -114,14 +114,14 @@
114
114
  "linkedom": "^0.18.12",
115
115
  "megalodon": "^10.3.0",
116
116
  "turndown": "^7.2.4",
117
- "@commonpub/docs": "0.6.3",
118
- "@commonpub/schema": "0.33.0",
119
- "@commonpub/protocol": "0.13.0",
120
- "@commonpub/editor": "0.7.11",
121
117
  "@commonpub/auth": "0.8.0",
122
- "@commonpub/config": "0.18.0",
118
+ "@commonpub/config": "0.19.0",
119
+ "@commonpub/learning": "0.5.2",
120
+ "@commonpub/protocol": "0.13.0",
121
+ "@commonpub/schema": "0.35.0",
123
122
  "@commonpub/infra": "0.8.0",
124
- "@commonpub/learning": "0.5.2"
123
+ "@commonpub/docs": "0.6.3",
124
+ "@commonpub/editor": "0.7.11"
125
125
  },
126
126
  "peerDependencies": {
127
127
  "drizzle-orm": "^0.45.1"