@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.
- package/dist/publicApi/cors.d.ts +51 -0
- package/dist/publicApi/cors.d.ts.map +1 -0
- package/dist/publicApi/cors.js +121 -0
- package/dist/publicApi/cors.js.map +1 -0
- package/dist/publicApi/index.d.ts +6 -0
- package/dist/publicApi/index.d.ts.map +1 -1
- package/dist/publicApi/index.js +3 -0
- package/dist/publicApi/index.js.map +1 -1
- package/dist/publicApi/metrics.d.ts +117 -0
- package/dist/publicApi/metrics.d.ts.map +1 -0
- package/dist/publicApi/metrics.js +317 -0
- package/dist/publicApi/metrics.js.map +1 -0
- package/dist/publicApi/metricsRollup.d.ts +55 -0
- package/dist/publicApi/metricsRollup.d.ts.map +1 -0
- package/dist/publicApi/metricsRollup.js +158 -0
- package/dist/publicApi/metricsRollup.js.map +1 -0
- package/package.json +7 -7
|
@@ -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"}
|
package/dist/publicApi/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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/
|
|
123
|
+
"@commonpub/docs": "0.6.3",
|
|
124
|
+
"@commonpub/editor": "0.7.11"
|
|
125
125
|
},
|
|
126
126
|
"peerDependencies": {
|
|
127
127
|
"drizzle-orm": "^0.45.1"
|