adaria-ai 0.1.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/LICENSE +21 -0
- package/README.md +21 -0
- package/apps.example.yaml +65 -0
- package/dist/agent/audit.d.ts +16 -0
- package/dist/agent/audit.d.ts.map +1 -0
- package/dist/agent/audit.js +42 -0
- package/dist/agent/audit.js.map +1 -0
- package/dist/agent/claude.d.ts +62 -0
- package/dist/agent/claude.d.ts.map +1 -0
- package/dist/agent/claude.js +297 -0
- package/dist/agent/claude.js.map +1 -0
- package/dist/agent/conversation-summary.d.ts +29 -0
- package/dist/agent/conversation-summary.d.ts.map +1 -0
- package/dist/agent/conversation-summary.js +221 -0
- package/dist/agent/conversation-summary.js.map +1 -0
- package/dist/agent/core.d.ts +81 -0
- package/dist/agent/core.d.ts.map +1 -0
- package/dist/agent/core.js +527 -0
- package/dist/agent/core.js.map +1 -0
- package/dist/agent/mcp-launcher.d.ts +42 -0
- package/dist/agent/mcp-launcher.d.ts.map +1 -0
- package/dist/agent/mcp-launcher.js +38 -0
- package/dist/agent/mcp-launcher.js.map +1 -0
- package/dist/agent/mcp-manager.d.ts +81 -0
- package/dist/agent/mcp-manager.d.ts.map +1 -0
- package/dist/agent/mcp-manager.js +136 -0
- package/dist/agent/mcp-manager.js.map +1 -0
- package/dist/agent/memory.d.ts +10 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +95 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/safety.d.ts +45 -0
- package/dist/agent/safety.d.ts.map +1 -0
- package/dist/agent/safety.js +71 -0
- package/dist/agent/safety.js.map +1 -0
- package/dist/agent/session.d.ts +27 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +124 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/agent/tool-descriptions.d.ts +8 -0
- package/dist/agent/tool-descriptions.d.ts.map +1 -0
- package/dist/agent/tool-descriptions.js +26 -0
- package/dist/agent/tool-descriptions.js.map +1 -0
- package/dist/cli/analyze.d.ts +8 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +114 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/cli/daemon.d.ts +2 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +91 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +198 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +459 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/logs.d.ts +4 -0
- package/dist/cli/logs.d.ts.map +1 -0
- package/dist/cli/logs.js +50 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor-cmd.d.ts +11 -0
- package/dist/cli/monitor-cmd.d.ts.map +1 -0
- package/dist/cli/monitor-cmd.js +59 -0
- package/dist/cli/monitor-cmd.js.map +1 -0
- package/dist/cli/start.d.ts +11 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +103 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +49 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/stop.d.ts +2 -0
- package/dist/cli/stop.d.ts.map +1 -0
- package/dist/cli/stop.js +34 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/collectors/appstore.d.ts +51 -0
- package/dist/collectors/appstore.d.ts.map +1 -0
- package/dist/collectors/appstore.js +166 -0
- package/dist/collectors/appstore.js.map +1 -0
- package/dist/collectors/arden-tts.d.ts +60 -0
- package/dist/collectors/arden-tts.d.ts.map +1 -0
- package/dist/collectors/arden-tts.js +83 -0
- package/dist/collectors/arden-tts.js.map +1 -0
- package/dist/collectors/asomobile.d.ts +37 -0
- package/dist/collectors/asomobile.d.ts.map +1 -0
- package/dist/collectors/asomobile.js +88 -0
- package/dist/collectors/asomobile.js.map +1 -0
- package/dist/collectors/eodin-blog.d.ts +90 -0
- package/dist/collectors/eodin-blog.d.ts.map +1 -0
- package/dist/collectors/eodin-blog.js +238 -0
- package/dist/collectors/eodin-blog.js.map +1 -0
- package/dist/collectors/eodin-sdk.d.ts +60 -0
- package/dist/collectors/eodin-sdk.d.ts.map +1 -0
- package/dist/collectors/eodin-sdk.js +112 -0
- package/dist/collectors/eodin-sdk.js.map +1 -0
- package/dist/collectors/fridgify-recipes.d.ts +65 -0
- package/dist/collectors/fridgify-recipes.d.ts.map +1 -0
- package/dist/collectors/fridgify-recipes.js +111 -0
- package/dist/collectors/fridgify-recipes.js.map +1 -0
- package/dist/collectors/playstore.d.ts +46 -0
- package/dist/collectors/playstore.d.ts.map +1 -0
- package/dist/collectors/playstore.js +140 -0
- package/dist/collectors/playstore.js.map +1 -0
- package/dist/collectors/youtube.d.ts +44 -0
- package/dist/collectors/youtube.d.ts.map +1 -0
- package/dist/collectors/youtube.js +107 -0
- package/dist/collectors/youtube.js.map +1 -0
- package/dist/config/apps-schema.d.ts +94 -0
- package/dist/config/apps-schema.d.ts.map +1 -0
- package/dist/config/apps-schema.js +66 -0
- package/dist/config/apps-schema.js.map +1 -0
- package/dist/config/keychain.d.ts +14 -0
- package/dist/config/keychain.d.ts.map +1 -0
- package/dist/config/keychain.js +89 -0
- package/dist/config/keychain.js.map +1 -0
- package/dist/config/load-apps.d.ts +16 -0
- package/dist/config/load-apps.d.ts.map +1 -0
- package/dist/config/load-apps.js +38 -0
- package/dist/config/load-apps.js.map +1 -0
- package/dist/config/schema.d.ts +306 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +220 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +38 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +180 -0
- package/dist/config/store.js.map +1 -0
- package/dist/db/queries.d.ts +304 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +327 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.d.ts +15 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +252 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/messenger/adapter.d.ts +63 -0
- package/dist/messenger/adapter.d.ts.map +1 -0
- package/dist/messenger/adapter.js +7 -0
- package/dist/messenger/adapter.js.map +1 -0
- package/dist/messenger/factory.d.ts +12 -0
- package/dist/messenger/factory.d.ts.map +1 -0
- package/dist/messenger/factory.js +9 -0
- package/dist/messenger/factory.js.map +1 -0
- package/dist/messenger/slack.d.ts +30 -0
- package/dist/messenger/slack.d.ts.map +1 -0
- package/dist/messenger/slack.js +309 -0
- package/dist/messenger/slack.js.map +1 -0
- package/dist/messenger/split.d.ts +17 -0
- package/dist/messenger/split.d.ts.map +1 -0
- package/dist/messenger/split.js +56 -0
- package/dist/messenger/split.js.map +1 -0
- package/dist/orchestrator/dashboard.d.ts +67 -0
- package/dist/orchestrator/dashboard.d.ts.map +1 -0
- package/dist/orchestrator/dashboard.js +113 -0
- package/dist/orchestrator/dashboard.js.map +1 -0
- package/dist/orchestrator/monitor.d.ts +37 -0
- package/dist/orchestrator/monitor.d.ts.map +1 -0
- package/dist/orchestrator/monitor.js +236 -0
- package/dist/orchestrator/monitor.js.map +1 -0
- package/dist/orchestrator/types.d.ts +82 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/orchestrator/types.js +12 -0
- package/dist/orchestrator/types.js.map +1 -0
- package/dist/orchestrator/weekly.d.ts +66 -0
- package/dist/orchestrator/weekly.d.ts.map +1 -0
- package/dist/orchestrator/weekly.js +376 -0
- package/dist/orchestrator/weekly.js.map +1 -0
- package/dist/prompts/loader.d.ts +18 -0
- package/dist/prompts/loader.d.ts.map +1 -0
- package/dist/prompts/loader.js +28 -0
- package/dist/prompts/loader.js.map +1 -0
- package/dist/security/auth.d.ts +14 -0
- package/dist/security/auth.d.ts.map +1 -0
- package/dist/security/auth.js +14 -0
- package/dist/security/auth.js.map +1 -0
- package/dist/security/prompt-guard.d.ts +21 -0
- package/dist/security/prompt-guard.d.ts.map +1 -0
- package/dist/security/prompt-guard.js +54 -0
- package/dist/security/prompt-guard.js.map +1 -0
- package/dist/skills/aso.d.ts +60 -0
- package/dist/skills/aso.d.ts.map +1 -0
- package/dist/skills/aso.js +322 -0
- package/dist/skills/aso.js.map +1 -0
- package/dist/skills/content.d.ts +25 -0
- package/dist/skills/content.d.ts.map +1 -0
- package/dist/skills/content.js +90 -0
- package/dist/skills/content.js.map +1 -0
- package/dist/skills/index.d.ts +65 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +90 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/onboarding.d.ts +58 -0
- package/dist/skills/onboarding.d.ts.map +1 -0
- package/dist/skills/onboarding.js +274 -0
- package/dist/skills/onboarding.js.map +1 -0
- package/dist/skills/registry.d.ts +24 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/registry.js +66 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/skills/review.d.ts +33 -0
- package/dist/skills/review.d.ts.map +1 -0
- package/dist/skills/review.js +236 -0
- package/dist/skills/review.js.map +1 -0
- package/dist/skills/sdk-request.d.ts +30 -0
- package/dist/skills/sdk-request.d.ts.map +1 -0
- package/dist/skills/sdk-request.js +72 -0
- package/dist/skills/sdk-request.js.map +1 -0
- package/dist/skills/seo-blog.d.ts +64 -0
- package/dist/skills/seo-blog.d.ts.map +1 -0
- package/dist/skills/seo-blog.js +268 -0
- package/dist/skills/seo-blog.js.map +1 -0
- package/dist/skills/short-form.d.ts +28 -0
- package/dist/skills/short-form.d.ts.map +1 -0
- package/dist/skills/short-form.js +121 -0
- package/dist/skills/short-form.js.map +1 -0
- package/dist/skills/social-publish.d.ts +32 -0
- package/dist/skills/social-publish.d.ts.map +1 -0
- package/dist/skills/social-publish.js +133 -0
- package/dist/skills/social-publish.js.map +1 -0
- package/dist/social/base.d.ts +47 -0
- package/dist/social/base.d.ts.map +1 -0
- package/dist/social/base.js +26 -0
- package/dist/social/base.js.map +1 -0
- package/dist/social/facebook.d.ts +27 -0
- package/dist/social/facebook.d.ts.map +1 -0
- package/dist/social/facebook.js +166 -0
- package/dist/social/facebook.js.map +1 -0
- package/dist/social/factory.d.ts +26 -0
- package/dist/social/factory.d.ts.map +1 -0
- package/dist/social/factory.js +32 -0
- package/dist/social/factory.js.map +1 -0
- package/dist/social/linkedin.d.ts +26 -0
- package/dist/social/linkedin.d.ts.map +1 -0
- package/dist/social/linkedin.js +190 -0
- package/dist/social/linkedin.js.map +1 -0
- package/dist/social/threads.d.ts +21 -0
- package/dist/social/threads.d.ts.map +1 -0
- package/dist/social/threads.js +122 -0
- package/dist/social/threads.js.map +1 -0
- package/dist/social/tiktok.d.ts +23 -0
- package/dist/social/tiktok.d.ts.map +1 -0
- package/dist/social/tiktok.js +110 -0
- package/dist/social/tiktok.js.map +1 -0
- package/dist/social/twitter.d.ts +30 -0
- package/dist/social/twitter.d.ts.map +1 -0
- package/dist/social/twitter.js +189 -0
- package/dist/social/twitter.js.map +1 -0
- package/dist/social/youtube.d.ts +21 -0
- package/dist/social/youtube.d.ts.map +1 -0
- package/dist/social/youtube.js +108 -0
- package/dist/social/youtube.js.map +1 -0
- package/dist/tools/app-info.d.ts +7 -0
- package/dist/tools/app-info.d.ts.map +1 -0
- package/dist/tools/app-info.js +53 -0
- package/dist/tools/app-info.js.map +1 -0
- package/dist/tools/collector-fetch.d.ts +11 -0
- package/dist/tools/collector-fetch.d.ts.map +1 -0
- package/dist/tools/collector-fetch.js +101 -0
- package/dist/tools/collector-fetch.js.map +1 -0
- package/dist/tools/db-query.d.ts +29 -0
- package/dist/tools/db-query.d.ts.map +1 -0
- package/dist/tools/db-query.js +159 -0
- package/dist/tools/db-query.js.map +1 -0
- package/dist/tools/skill-result.d.ts +8 -0
- package/dist/tools/skill-result.d.ts.map +1 -0
- package/dist/tools/skill-result.js +63 -0
- package/dist/tools/skill-result.js.map +1 -0
- package/dist/tools/tool-host.d.ts +12 -0
- package/dist/tools/tool-host.d.ts.map +1 -0
- package/dist/tools/tool-host.js +124 -0
- package/dist/tools/tool-host.js.map +1 -0
- package/dist/types/collectors.d.ts +198 -0
- package/dist/types/collectors.d.ts.map +1 -0
- package/dist/types/collectors.js +28 -0
- package/dist/types/collectors.js.map +1 -0
- package/dist/types/skill.d.ts +60 -0
- package/dist/types/skill.d.ts.map +1 -0
- package/dist/types/skill.js +9 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/circuit-breaker.d.ts +26 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +67 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +75 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/escape.d.ts +11 -0
- package/dist/utils/escape.d.ts.map +1 -0
- package/dist/utils/escape.js +19 -0
- package/dist/utils/escape.js.map +1 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +93 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/parse-json.d.ts +13 -0
- package/dist/utils/parse-json.d.ts.map +1 -0
- package/dist/utils/parse-json.js +61 -0
- package/dist/utils/parse-json.js.map +1 -0
- package/dist/utils/paths.d.ts +14 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +19 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +20 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +47 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +26 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/retry.js.map +1 -0
- package/launchd/.gitkeep +0 -0
- package/launchd/com.adaria-ai.daemon.plist.template +62 -0
- package/launchd/com.adaria-ai.monitor.plist.template +41 -0
- package/launchd/com.adaria-ai.weekly.plist.template +43 -0
- package/package.json +72 -0
- package/prompts/aso-description.md +44 -0
- package/prompts/aso-inapp-events.md +20 -0
- package/prompts/aso-metadata.md +34 -0
- package/prompts/aso-screenshots.md +20 -0
- package/prompts/onboarding-hypotheses.md +38 -0
- package/prompts/onboarding-review-timing.md +24 -0
- package/prompts/review-clustering.md +19 -0
- package/prompts/review-replies.md +18 -0
- package/prompts/review-sentiment.md +16 -0
- package/prompts/seo-blog-fridgify-recipe.md +116 -0
- package/prompts/seo-blog.md +69 -0
- package/prompts/short-form-ideas.md +50 -0
- package/prompts/social-publish.md +46 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ExternalApiError } from "../utils/errors.js";
|
|
2
|
+
const ALLOWED_HOSTS = new Set(["api.eodin.app"]);
|
|
3
|
+
const DEFAULT_BASE_URL = "https://api.eodin.app/api/v1/events";
|
|
4
|
+
const ERROR_BODY_MAX_CHARS = 512;
|
|
5
|
+
export class EodinSdkCollector {
|
|
6
|
+
apiKey;
|
|
7
|
+
baseUrl;
|
|
8
|
+
loggedPercentCohort = false;
|
|
9
|
+
constructor(options, testHooks) {
|
|
10
|
+
if (!options.apiKey) {
|
|
11
|
+
throw new Error("EodinSdkCollector requires apiKey");
|
|
12
|
+
}
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
|
|
15
|
+
}
|
|
16
|
+
async request(path, params = {}) {
|
|
17
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
18
|
+
if (!ALLOWED_HOSTS.has(url.hostname)) {
|
|
19
|
+
throw new Error(`Untrusted SDK host: ${url.hostname}. Allowed: ${[...ALLOWED_HOSTS].join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, value] of Object.entries(params)) {
|
|
22
|
+
// `null` is a defensive check for loose callers even though QueryParams
|
|
23
|
+
// doesn't declare it — leave both branches in.
|
|
24
|
+
if (value === undefined || value === null || value === "")
|
|
25
|
+
continue;
|
|
26
|
+
url.searchParams.set(key, String(value));
|
|
27
|
+
}
|
|
28
|
+
const response = await fetch(url.toString(), {
|
|
29
|
+
headers: {
|
|
30
|
+
"X-API-Key": this.apiKey,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const rawBody = await response.text();
|
|
36
|
+
// The Eodin server has historically echoed the submitted API key back
|
|
37
|
+
// in error bodies (`{"error":"Invalid X-API-Key: <key>"}`). Redact
|
|
38
|
+
// before the message reaches audit logs or Slack error cards.
|
|
39
|
+
const redacted = rawBody
|
|
40
|
+
.replaceAll(this.apiKey, "[REDACTED]")
|
|
41
|
+
.slice(0, ERROR_BODY_MAX_CHARS);
|
|
42
|
+
throw new ExternalApiError(`Eodin SDK API ${String(response.status)}: ${redacted}`, { statusCode: response.status });
|
|
43
|
+
}
|
|
44
|
+
return (await response.json());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Daily/weekly/monthly aggregate rows.
|
|
48
|
+
*/
|
|
49
|
+
async getSummary(appId, startDate, endDate, options = {}) {
|
|
50
|
+
const res = await this.request("/summary", {
|
|
51
|
+
app_id: appId,
|
|
52
|
+
start: startDate,
|
|
53
|
+
end: endDate,
|
|
54
|
+
granularity: options.granularity ?? "daily",
|
|
55
|
+
os: options.os ?? "all",
|
|
56
|
+
});
|
|
57
|
+
return res.data ?? [];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Aggregate funnel for the period. Step order is fixed by the Eodin API:
|
|
61
|
+
* `app_install → app_open → core_action → paywall_view → subscribe_start`.
|
|
62
|
+
*/
|
|
63
|
+
async getFunnel(appId, startDate, endDate, options = {}) {
|
|
64
|
+
const res = await this.request("/funnel", {
|
|
65
|
+
app_id: appId,
|
|
66
|
+
start: startDate,
|
|
67
|
+
end: endDate,
|
|
68
|
+
source: options.source,
|
|
69
|
+
os: options.os ?? "all",
|
|
70
|
+
});
|
|
71
|
+
return res.data ?? { funnel: [], overall_conversion: 0 };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Cohort retention. `retention[0]` is always the cohort anchor (100% of
|
|
75
|
+
* the cohort by definition), so any value above 1.5 signals the server
|
|
76
|
+
* returned percents instead of fractions; we detect and normalize so
|
|
77
|
+
* every downstream consumer sees fractions in [0, 1].
|
|
78
|
+
*/
|
|
79
|
+
async getCohort(appId, startDate, endDate, options = {}) {
|
|
80
|
+
const res = await this.request("/cohort", {
|
|
81
|
+
app_id: appId,
|
|
82
|
+
start: startDate,
|
|
83
|
+
end: endDate,
|
|
84
|
+
granularity: options.granularity ?? "weekly",
|
|
85
|
+
os: options.os ?? "all",
|
|
86
|
+
});
|
|
87
|
+
const cohorts = res.data?.cohorts ?? [];
|
|
88
|
+
return cohorts.map((c) => ({
|
|
89
|
+
...c,
|
|
90
|
+
retention: this.normalizeRetention(c.retention),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
normalizeRetention(retention) {
|
|
94
|
+
// Wire is typed as number[] but the Eodin server has historically
|
|
95
|
+
// returned nulls/strings inside the array, so the typeof guards below
|
|
96
|
+
// are intentional runtime defense — not dead code the strict types
|
|
97
|
+
// would otherwise suggest.
|
|
98
|
+
if (!Array.isArray(retention) || retention.length === 0) {
|
|
99
|
+
return retention;
|
|
100
|
+
}
|
|
101
|
+
const first = retention[0];
|
|
102
|
+
if (typeof first !== "number" || first <= 1.5) {
|
|
103
|
+
return retention;
|
|
104
|
+
}
|
|
105
|
+
if (!this.loggedPercentCohort) {
|
|
106
|
+
console.warn("[eodin-sdk] Detected percent-encoded cohort retention; normalizing to fractions.");
|
|
107
|
+
this.loggedPercentCohort = true;
|
|
108
|
+
}
|
|
109
|
+
return retention.map((r) => (typeof r === "number" ? r / 100 : r));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=eodin-sdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eodin-sdk.js","sourceRoot":"","sources":["../../src/collectors/eodin-sdk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { FridgifyCascadeResult, FridgifyPeriod, FridgifyPopularMetric, FridgifyRecipe } from "../types/collectors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fridgify Recipes API collector.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around the Fridgify backend's public recipe endpoints that
|
|
6
|
+
* power the growth agent's recipe-aware blog posts.
|
|
7
|
+
*
|
|
8
|
+
* - Base URL: `https://fridgify-api.eodin.app`
|
|
9
|
+
* - Auth: none (public endpoints, IP rate-limited 20 req/min)
|
|
10
|
+
*/
|
|
11
|
+
export interface FridgifyRecipesCollectorOptions {
|
|
12
|
+
/** Override the rate-limit backoff window. Defaults to 60 s. */
|
|
13
|
+
retryDelayMs?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Test-only overrides. `baseUrl` stays off the production options type so
|
|
17
|
+
* config loaders cannot introduce a user-controlled URL into the SSRF
|
|
18
|
+
* surface; the allowlist still applies to test-hook values.
|
|
19
|
+
*/
|
|
20
|
+
export interface FridgifyRecipesCollectorTestHooks {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface GetPopularOptions {
|
|
24
|
+
period?: FridgifyPeriod;
|
|
25
|
+
metric?: FridgifyPopularMetric;
|
|
26
|
+
/** Server clamps to 1–50. */
|
|
27
|
+
limit?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CascadeOptions {
|
|
30
|
+
metric?: FridgifyPopularMetric;
|
|
31
|
+
limit?: number;
|
|
32
|
+
/** Narrowest window is accepted once `rows.length >= minResults`. */
|
|
33
|
+
minResults?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class FridgifyRecipesCollector {
|
|
36
|
+
private readonly baseUrl;
|
|
37
|
+
private readonly retryDelayMs;
|
|
38
|
+
constructor(options?: FridgifyRecipesCollectorOptions, testHooks?: FridgifyRecipesCollectorTestHooks);
|
|
39
|
+
private fetchOnce;
|
|
40
|
+
private request;
|
|
41
|
+
/**
|
|
42
|
+
* Top recipes in a time window, ranked by engagement.
|
|
43
|
+
*/
|
|
44
|
+
getPopular(options?: GetPopularOptions): Promise<FridgifyRecipe[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Period-cascade variant of {@link getPopular}.
|
|
47
|
+
*
|
|
48
|
+
* Fridgify's `week` window is frequently empty under current traffic.
|
|
49
|
+
* Walk week → month → quarter → year and stop at the narrowest window
|
|
50
|
+
* that yields at least `minResults` rows, so blog copy naturally stays
|
|
51
|
+
* fresh ("Top recipes this week") without the skill giving up when the
|
|
52
|
+
* week is quiet.
|
|
53
|
+
*
|
|
54
|
+
* Callers that need a roundup-worthy result should branch on
|
|
55
|
+
* {@link FridgifyCascadeResult.satisfied}, not on `rows.length > 0`, to
|
|
56
|
+
* avoid building a "top recipes this year" post from a single stray row.
|
|
57
|
+
*/
|
|
58
|
+
getPopularWithCascade(options?: CascadeOptions): Promise<FridgifyCascadeResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Fetch a single recipe by id. Returns the same shape as items in
|
|
61
|
+
* {@link getPopular} minus `periodScore`.
|
|
62
|
+
*/
|
|
63
|
+
getRecipe(id: string): Promise<FridgifyRecipe>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=fridgify-recipes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fridgify-recipes.d.ts","sourceRoot":"","sources":["../../src/collectors/fridgify-recipes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACf,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,WAAW,+BAA+B;IAC9C,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGpC,OAAO,GAAE,+BAAoC,EAC7C,SAAS,CAAC,EAAE,iCAAiC;YAMjC,SAAS;YAMT,OAAO;IAmDrB;;OAEG;IACG,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAa5E;;;;;;;;;;;;OAYG;IACG,qBAAqB,CACzB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA0BjC;;;OAGG;IACG,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAUrD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ExternalApiError, RateLimitError } from "../utils/errors.js";
|
|
2
|
+
import { info as logInfo, warn as logWarn } from "../utils/logger.js";
|
|
3
|
+
const DEFAULT_BASE_URL = "https://fridgify-api.eodin.app";
|
|
4
|
+
const DEFAULT_RETRY_DELAY_MS = 60_000;
|
|
5
|
+
const ALLOWED_HOSTS = new Set([
|
|
6
|
+
"fridgify-api.eodin.app",
|
|
7
|
+
]);
|
|
8
|
+
const CASCADE_PERIODS = [
|
|
9
|
+
"week",
|
|
10
|
+
"month",
|
|
11
|
+
"quarter",
|
|
12
|
+
"year",
|
|
13
|
+
];
|
|
14
|
+
export class FridgifyRecipesCollector {
|
|
15
|
+
baseUrl;
|
|
16
|
+
retryDelayMs;
|
|
17
|
+
constructor(options = {}, testHooks) {
|
|
18
|
+
this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
|
|
19
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
20
|
+
}
|
|
21
|
+
async fetchOnce(url) {
|
|
22
|
+
return fetch(url, {
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async request(path, params = {}) {
|
|
27
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
28
|
+
if (!ALLOWED_HOSTS.has(url.hostname)) {
|
|
29
|
+
throw new Error(`Untrusted Fridgify host: ${url.hostname}. Allowed: ${[...ALLOWED_HOSTS].join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
for (const [key, value] of Object.entries(params)) {
|
|
32
|
+
if (value === undefined || value === "")
|
|
33
|
+
continue;
|
|
34
|
+
url.searchParams.set(key, String(value));
|
|
35
|
+
}
|
|
36
|
+
const target = url.toString();
|
|
37
|
+
let response = await this.fetchOnce(target);
|
|
38
|
+
// The endpoint is capped at 20 req/min per IP. Retry exactly once after
|
|
39
|
+
// the configured backoff so scheduled weekly runs can ride through a
|
|
40
|
+
// bursty neighbor on the shared IP. Retrying more aggressively would
|
|
41
|
+
// just waste the budget.
|
|
42
|
+
if (response.status === 429) {
|
|
43
|
+
logWarn(`[fridgify-recipes] 429 on ${path}; waiting ${String(this.retryDelayMs)}ms before one retry`);
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs));
|
|
45
|
+
response = await this.fetchOnce(target);
|
|
46
|
+
}
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const body = await response.text();
|
|
49
|
+
if (response.status === 429) {
|
|
50
|
+
throw new RateLimitError(`Fridgify API still rate limited after 1 retry: ${body.slice(0, 512)}`, { retryAfterSeconds: Math.ceil(this.retryDelayMs / 1000) });
|
|
51
|
+
}
|
|
52
|
+
throw new ExternalApiError(`Fridgify API ${String(response.status)}: ${body.slice(0, 512)}`, { statusCode: response.status });
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Top recipes in a time window, ranked by engagement.
|
|
58
|
+
*/
|
|
59
|
+
async getPopular(options = {}) {
|
|
60
|
+
const period = options.period ?? "week";
|
|
61
|
+
const metric = options.metric ?? "combined";
|
|
62
|
+
const limit = options.limit ?? 10;
|
|
63
|
+
const data = await this.request("/recipes/popular", {
|
|
64
|
+
period,
|
|
65
|
+
metric,
|
|
66
|
+
limit,
|
|
67
|
+
});
|
|
68
|
+
return Array.isArray(data) ? data : [];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Period-cascade variant of {@link getPopular}.
|
|
72
|
+
*
|
|
73
|
+
* Fridgify's `week` window is frequently empty under current traffic.
|
|
74
|
+
* Walk week → month → quarter → year and stop at the narrowest window
|
|
75
|
+
* that yields at least `minResults` rows, so blog copy naturally stays
|
|
76
|
+
* fresh ("Top recipes this week") without the skill giving up when the
|
|
77
|
+
* week is quiet.
|
|
78
|
+
*
|
|
79
|
+
* Callers that need a roundup-worthy result should branch on
|
|
80
|
+
* {@link FridgifyCascadeResult.satisfied}, not on `rows.length > 0`, to
|
|
81
|
+
* avoid building a "top recipes this year" post from a single stray row.
|
|
82
|
+
*/
|
|
83
|
+
async getPopularWithCascade(options = {}) {
|
|
84
|
+
const metric = options.metric ?? "combined";
|
|
85
|
+
const limit = options.limit ?? 10;
|
|
86
|
+
const minResults = options.minResults ?? 5;
|
|
87
|
+
let lastRows = [];
|
|
88
|
+
for (const period of CASCADE_PERIODS) {
|
|
89
|
+
const rows = await this.getPopular({ period, metric, limit });
|
|
90
|
+
lastRows = rows;
|
|
91
|
+
if (rows.length >= minResults) {
|
|
92
|
+
logInfo(`[fridgify-recipes] cascade stopped at period=${period} (${String(rows.length)} rows)`);
|
|
93
|
+
return { period, rows, satisfied: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const finalPeriod = CASCADE_PERIODS[CASCADE_PERIODS.length - 1] ?? "year";
|
|
97
|
+
logWarn(`[fridgify-recipes] cascade exhausted — no window had >=${String(minResults)} rows (last=${String(lastRows.length)})`);
|
|
98
|
+
return { period: finalPeriod, rows: lastRows, satisfied: false };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fetch a single recipe by id. Returns the same shape as items in
|
|
102
|
+
* {@link getPopular} minus `periodScore`.
|
|
103
|
+
*/
|
|
104
|
+
async getRecipe(id) {
|
|
105
|
+
if (id.length === 0) {
|
|
106
|
+
throw new Error("FridgifyRecipesCollector.getRecipe requires a non-empty string id");
|
|
107
|
+
}
|
|
108
|
+
return this.request(`/recipes/${encodeURIComponent(id)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=fridgify-recipes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fridgify-recipes.js","sourceRoot":"","sources":["../../src/collectors/fridgify-recipes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StoreReview } from "../types/collectors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Google Play Developer API collector.
|
|
4
|
+
* Uses a Service Account JSON for authentication via JWT → OAuth token exchange.
|
|
5
|
+
*
|
|
6
|
+
* @see https://developers.google.com/android-publisher
|
|
7
|
+
*/
|
|
8
|
+
export interface PlayStoreServiceAccount {
|
|
9
|
+
client_email: string;
|
|
10
|
+
private_key: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PlayStoreCollectorOptions {
|
|
13
|
+
serviceAccountJson: PlayStoreServiceAccount | string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Test-only overrides. Kept off {@link PlayStoreCollectorOptions} so
|
|
17
|
+
* production config loaders cannot feed a user-controlled URL into the
|
|
18
|
+
* SSRF surface.
|
|
19
|
+
*/
|
|
20
|
+
export interface PlayStoreCollectorTestHooks {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
tokenUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class PlayStoreCollector {
|
|
25
|
+
private readonly serviceAccount;
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly tokenUrl;
|
|
28
|
+
private accessToken;
|
|
29
|
+
private tokenExpiresAt;
|
|
30
|
+
constructor(options: PlayStoreCollectorOptions, testHooks?: PlayStoreCollectorTestHooks);
|
|
31
|
+
private getAccessToken;
|
|
32
|
+
private request;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch reviews for a package.
|
|
35
|
+
*/
|
|
36
|
+
getReviews(packageName: string): Promise<StoreReview[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch app listing details (title, short/full description) for a locale.
|
|
39
|
+
*/
|
|
40
|
+
getAppDetails(packageName: string, locale?: string): Promise<unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Reply to a review on Google Play. Approval-gated write path.
|
|
43
|
+
*/
|
|
44
|
+
replyToReview(packageName: string, reviewId: string, replyText: string): Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=playstore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playstore.d.ts","sourceRoot":"","sources":["../../src/collectors/playstore.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,EAAE,uBAAuB,GAAG,MAAM,CAAC;CACtD;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA+BD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0B;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAK;gBAGzB,OAAO,EAAE,yBAAyB,EAClC,SAAS,CAAC,EAAE,2BAA2B;YA+B3B,cAAc;YAsCd,OAAO;IAmCrB;;OAEG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0B7D;;OAEG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAM5E;;OAEG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;CAiBpB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
2
|
+
import { AuthError, ExternalApiError, RateLimitError } from "../utils/errors.js";
|
|
3
|
+
import { parseRetryAfter } from "../utils/retry.js";
|
|
4
|
+
const DEFAULT_BASE_URL = "https://androidpublisher.googleapis.com/androidpublisher/v3";
|
|
5
|
+
const DEFAULT_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
6
|
+
const REPLY_LIMIT = 350;
|
|
7
|
+
export class PlayStoreCollector {
|
|
8
|
+
serviceAccount;
|
|
9
|
+
baseUrl;
|
|
10
|
+
tokenUrl;
|
|
11
|
+
accessToken = null;
|
|
12
|
+
tokenExpiresAt = 0;
|
|
13
|
+
constructor(options, testHooks) {
|
|
14
|
+
if (!options.serviceAccountJson) {
|
|
15
|
+
throw new Error("PlayStoreCollector requires serviceAccountJson");
|
|
16
|
+
}
|
|
17
|
+
if (typeof options.serviceAccountJson === "string") {
|
|
18
|
+
try {
|
|
19
|
+
this.serviceAccount = JSON.parse(options.serviceAccountJson);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Never surface parser errors verbatim — the raw text may contain
|
|
23
|
+
// a leading fragment of the private key.
|
|
24
|
+
throw new AuthError("PlayStoreCollector: serviceAccountJson is not valid JSON");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.serviceAccount = options.serviceAccountJson;
|
|
29
|
+
}
|
|
30
|
+
if (!this.serviceAccount.client_email || !this.serviceAccount.private_key) {
|
|
31
|
+
throw new Error("PlayStoreCollector: serviceAccountJson must include client_email and private_key");
|
|
32
|
+
}
|
|
33
|
+
this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
|
|
34
|
+
this.tokenUrl = testHooks?.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
35
|
+
}
|
|
36
|
+
async getAccessToken() {
|
|
37
|
+
if (this.accessToken && Date.now() < this.tokenExpiresAt) {
|
|
38
|
+
return this.accessToken;
|
|
39
|
+
}
|
|
40
|
+
const key = await importPKCS8(this.serviceAccount.private_key, "RS256");
|
|
41
|
+
const jwt = await new SignJWT({
|
|
42
|
+
scope: "https://www.googleapis.com/auth/androidpublisher",
|
|
43
|
+
})
|
|
44
|
+
.setProtectedHeader({ alg: "RS256", typ: "JWT" })
|
|
45
|
+
.setIssuer(this.serviceAccount.client_email)
|
|
46
|
+
.setAudience(this.tokenUrl)
|
|
47
|
+
.setIssuedAt()
|
|
48
|
+
.setExpirationTime("1h")
|
|
49
|
+
.sign(key);
|
|
50
|
+
const response = await fetch(this.tokenUrl, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
55
|
+
assertion: jwt,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const body = await response.text();
|
|
60
|
+
throw new ExternalApiError(`Google OAuth failed: ${body}`, {
|
|
61
|
+
statusCode: response.status,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const data = (await response.json());
|
|
65
|
+
this.accessToken = data.access_token;
|
|
66
|
+
this.tokenExpiresAt = Date.now() + (data.expires_in - 60) * 1000;
|
|
67
|
+
return this.accessToken;
|
|
68
|
+
}
|
|
69
|
+
async request(path, init) {
|
|
70
|
+
const token = await this.getAccessToken();
|
|
71
|
+
const url = `${this.baseUrl}${path}`;
|
|
72
|
+
const headers = {
|
|
73
|
+
Authorization: `Bearer ${token}`,
|
|
74
|
+
...(init?.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
75
|
+
...(init?.headers ?? {}),
|
|
76
|
+
};
|
|
77
|
+
const fetchInit = { headers };
|
|
78
|
+
if (init?.method !== undefined)
|
|
79
|
+
fetchInit.method = init.method;
|
|
80
|
+
if (init?.body !== undefined)
|
|
81
|
+
fetchInit.body = init.body;
|
|
82
|
+
const response = await fetch(url, fetchInit);
|
|
83
|
+
if (response.status === 429) {
|
|
84
|
+
throw new RateLimitError("Google Play API rate limited", {
|
|
85
|
+
retryAfterSeconds: parseRetryAfter(response.headers.get("Retry-After")),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const body = await response.text();
|
|
90
|
+
throw new ExternalApiError(`Google Play API ${String(response.status)}: ${body}`, { statusCode: response.status });
|
|
91
|
+
}
|
|
92
|
+
return (await response.json());
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch reviews for a package.
|
|
96
|
+
*/
|
|
97
|
+
async getReviews(packageName) {
|
|
98
|
+
const data = await this.request(`/applications/${packageName}/reviews`);
|
|
99
|
+
return (data.reviews ?? []).map((r) => {
|
|
100
|
+
const comment = r.comments?.[0]?.userComment;
|
|
101
|
+
const rawSeconds = comment?.lastModified?.seconds;
|
|
102
|
+
const seconds = typeof rawSeconds === "string"
|
|
103
|
+
? Number.parseInt(rawSeconds, 10)
|
|
104
|
+
: typeof rawSeconds === "number"
|
|
105
|
+
? rawSeconds
|
|
106
|
+
: null;
|
|
107
|
+
const createdAt = seconds !== null && Number.isFinite(seconds)
|
|
108
|
+
? new Date(seconds * 1000).toISOString()
|
|
109
|
+
: null;
|
|
110
|
+
return {
|
|
111
|
+
reviewId: r.reviewId,
|
|
112
|
+
rating: comment?.starRating ?? 0,
|
|
113
|
+
body: comment?.text ?? "",
|
|
114
|
+
createdAt,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fetch app listing details (title, short/full description) for a locale.
|
|
120
|
+
*/
|
|
121
|
+
async getAppDetails(packageName, locale = "ko-KR") {
|
|
122
|
+
return this.request(`/applications/${packageName}/edits/-/listings/${locale}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Reply to a review on Google Play. Approval-gated write path.
|
|
126
|
+
*/
|
|
127
|
+
async replyToReview(packageName, reviewId, replyText) {
|
|
128
|
+
if (typeof replyText !== "string" || replyText.trim().length === 0) {
|
|
129
|
+
throw new Error("replyToReview: replyText must be a non-empty string");
|
|
130
|
+
}
|
|
131
|
+
if (replyText.length > REPLY_LIMIT) {
|
|
132
|
+
throw new Error(`replyToReview: replyText exceeds Google Play ${String(REPLY_LIMIT)} char limit (${String(replyText.length)})`);
|
|
133
|
+
}
|
|
134
|
+
return this.request(`/applications/${packageName}/reviews/${reviewId}:reply`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({ replyText }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=playstore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playstore.js","sourceRoot":"","sources":["../../src/collectors/playstore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { YouTubeVideoStats } from "../types/collectors.js";
|
|
2
|
+
/**
|
|
3
|
+
* YouTube Data API v3 client for collecting Shorts performance metrics.
|
|
4
|
+
*
|
|
5
|
+
* Used by `ShortFormSkill` (M5) to pull recent Shorts from a channel and
|
|
6
|
+
* fetch their view / like / comment counts so the weekly briefing can
|
|
7
|
+
* show week-over-week changes.
|
|
8
|
+
*
|
|
9
|
+
* @see https://developers.google.com/youtube/v3
|
|
10
|
+
*/
|
|
11
|
+
export interface YouTubeCollectorOptions {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Test-only overrides. `baseUrl` is intentionally kept off
|
|
16
|
+
* {@link YouTubeCollectorOptions} so production config loaders cannot
|
|
17
|
+
* override Google's API host; the allowlist still gates any test-hook
|
|
18
|
+
* value as defense-in-depth.
|
|
19
|
+
*/
|
|
20
|
+
export interface YouTubeCollectorTestHooks {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class YouTubeCollector {
|
|
24
|
+
private readonly apiKey;
|
|
25
|
+
private readonly baseUrl;
|
|
26
|
+
constructor(options: YouTubeCollectorOptions, testHooks?: YouTubeCollectorTestHooks);
|
|
27
|
+
private redact;
|
|
28
|
+
private fetchJson;
|
|
29
|
+
/**
|
|
30
|
+
* Get recent Shorts videos from a channel.
|
|
31
|
+
*
|
|
32
|
+
* YouTube's `videoDuration=short` search filter returns clips under
|
|
33
|
+
* **4 minutes** (not 60 seconds). We cross-check the content-details
|
|
34
|
+
* duration against `maxDurationSeconds` (default 60) so callers get a
|
|
35
|
+
* correct "Shorts" bucket by default. Pass a larger value (e.g. 180)
|
|
36
|
+
* if you want YouTube's looser "short-form" definition.
|
|
37
|
+
*/
|
|
38
|
+
getRecentShorts(channelId: string, maxResults?: number, maxDurationSeconds?: number): Promise<YouTubeVideoStats[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Get statistics for specific video IDs.
|
|
41
|
+
*/
|
|
42
|
+
getVideoStats(videoIds: string[]): Promise<YouTubeVideoStats[]>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=youtube.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../../src/collectors/youtube.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAKD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAG/B,OAAO,EAAE,uBAAuB,EAChC,SAAS,CAAC,EAAE,yBAAyB;IASvC,OAAO,CAAC,MAAM;YAIA,SAAS;IAgCvB;;;;;;;;OAQG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,SAAK,EACf,kBAAkB,SAAK,GACtB,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA2B/B;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAkCtE"}
|