@yourbright/emdash-analytics-plugin 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,153 +0,0 @@
1
- import { z } from "astro/zod";
2
-
3
- import type { GoogleServiceAccount, SavedPluginConfig } from "./types.js";
4
-
5
- export interface ConfigDraftInput {
6
- siteOrigin?: string;
7
- ga4PropertyId?: string;
8
- gscSiteUrl?: string;
9
- serviceAccountJson?: string;
10
- }
11
-
12
- type ValidationResult<T> =
13
- | { success: true; data: T }
14
- | { success: false; message: string };
15
-
16
- const serviceAccountSchema = z.object({
17
- client_email: z.string().email("Service Account JSON must include a valid client_email"),
18
- private_key: z.string().min(1, "Service Account JSON must include a private_key"),
19
- token_uri: z.string().url().optional()
20
- });
21
-
22
- const savedConfigSchema = z.object({
23
- siteOrigin: z
24
- .string()
25
- .min(1, "Canonical Site Origin is required")
26
- .refine(isHttpUrl, "Canonical Site Origin must be a valid http(s) URL"),
27
- ga4PropertyId: z
28
- .string()
29
- .min(1, "GA4 Property ID is required")
30
- .regex(/^[0-9]+$/, "GA4 Property ID must be numeric"),
31
- gscSiteUrl: z
32
- .string()
33
- .min(1, "Search Console Property is required")
34
- .refine(isValidSearchConsoleProperty, "Search Console Property must be a valid URL or sc-domain property"),
35
- serviceAccountJson: z
36
- .string()
37
- .min(1, "Service Account JSON is required")
38
- .superRefine((value, ctx) => {
39
- const parsed = parseServiceAccountSafe(value);
40
- if (!parsed.success) {
41
- ctx.addIssue({
42
- code: z.ZodIssueCode.custom,
43
- message: parsed.message
44
- });
45
- }
46
- })
47
- });
48
-
49
- export function resolveConfigInput(
50
- input: ConfigDraftInput | null | undefined,
51
- current: SavedPluginConfig | null = null
52
- ): ValidationResult<SavedPluginConfig> {
53
- const candidate = {
54
- siteOrigin: resolveField(input?.siteOrigin, current?.siteOrigin),
55
- ga4PropertyId: resolveField(input?.ga4PropertyId, current?.ga4PropertyId),
56
- gscSiteUrl: resolveField(input?.gscSiteUrl, current?.gscSiteUrl),
57
- serviceAccountJson: resolveServiceAccountField(input?.serviceAccountJson, current?.serviceAccountJson)
58
- };
59
-
60
- const parsed = savedConfigSchema.safeParse(candidate);
61
- if (!parsed.success) {
62
- return {
63
- success: false,
64
- message: formatValidationError(parsed.error)
65
- };
66
- }
67
-
68
- return {
69
- success: true,
70
- data: {
71
- ...parsed.data,
72
- siteOrigin: normalizeOrigin(parsed.data.siteOrigin)
73
- }
74
- };
75
- }
76
-
77
- export function parseServiceAccount(json: string): GoogleServiceAccount {
78
- const result = parseServiceAccountSafe(json);
79
- if (!result.success) {
80
- throw new Error(result.message);
81
- }
82
- return result.data;
83
- }
84
-
85
- export function normalizeOrigin(origin: string): string {
86
- const url = new URL(origin);
87
- url.pathname = "/";
88
- url.search = "";
89
- url.hash = "";
90
- return url.origin;
91
- }
92
-
93
- function parseServiceAccountSafe(json: string): ValidationResult<GoogleServiceAccount> {
94
- let parsedJson: unknown;
95
- try {
96
- parsedJson = JSON.parse(json);
97
- } catch {
98
- return {
99
- success: false,
100
- message: "Service Account JSON must be valid JSON"
101
- };
102
- }
103
-
104
- const parsed = serviceAccountSchema.safeParse(parsedJson);
105
- if (!parsed.success) {
106
- return {
107
- success: false,
108
- message: formatValidationError(parsed.error)
109
- };
110
- }
111
-
112
- return {
113
- success: true,
114
- data: parsed.data
115
- };
116
- }
117
-
118
- function resolveField(value: string | undefined, fallback: string | undefined): string {
119
- if (value === undefined) {
120
- return fallback ?? "";
121
- }
122
- const trimmed = value.trim();
123
- return trimmed.length > 0 ? trimmed : fallback ?? "";
124
- }
125
-
126
- function resolveServiceAccountField(value: string | undefined, fallback: string | undefined): string {
127
- if (value === undefined) {
128
- return fallback ?? "";
129
- }
130
- const trimmed = value.trim();
131
- return trimmed.length > 0 ? trimmed : fallback ?? "";
132
- }
133
-
134
- function isHttpUrl(value: string): boolean {
135
- try {
136
- const url = new URL(value);
137
- return url.protocol === "http:" || url.protocol === "https:";
138
- } catch {
139
- return false;
140
- }
141
- }
142
-
143
- function isValidSearchConsoleProperty(value: string): boolean {
144
- if (value.startsWith("sc-domain:")) {
145
- return value.slice("sc-domain:".length).trim().length > 0;
146
- }
147
- return isHttpUrl(value);
148
- }
149
-
150
- function formatValidationError(error: z.ZodError): string {
151
- const messages = Array.from(new Set(error.issues.map((issue) => issue.message).filter(Boolean)));
152
- return messages[0] || "Invalid settings";
153
- }
package/src/config.ts DELETED
@@ -1,90 +0,0 @@
1
- import { decrypt, encrypt } from "@emdash-cms/auth";
2
- import type { PluginContext } from "emdash";
3
-
4
- import {
5
- CONFIG_GA4_PROPERTY_ID_KEY,
6
- CONFIG_GSC_SITE_URL_KEY,
7
- CONFIG_SERVICE_ACCOUNT_KEY,
8
- CONFIG_SITE_ORIGIN_KEY
9
- } from "./constants.js";
10
- import { normalizeOrigin, parseServiceAccount, resolveConfigInput } from "./config-validation.js";
11
- import type { PluginConfigSummary, SavedPluginConfig } from "./types.js";
12
-
13
- type PluginCtx = PluginContext;
14
-
15
- export async function loadConfig(ctx: PluginCtx): Promise<SavedPluginConfig | null> {
16
- const [siteOrigin, ga4PropertyId, gscSiteUrl, serviceAccountCiphertext] = await Promise.all([
17
- ctx.kv.get<string>(CONFIG_SITE_ORIGIN_KEY),
18
- ctx.kv.get<string>(CONFIG_GA4_PROPERTY_ID_KEY),
19
- ctx.kv.get<string>(CONFIG_GSC_SITE_URL_KEY),
20
- ctx.kv.get<string>(CONFIG_SERVICE_ACCOUNT_KEY)
21
- ]);
22
-
23
- if (!siteOrigin || !ga4PropertyId || !gscSiteUrl || !serviceAccountCiphertext) {
24
- return null;
25
- }
26
-
27
- const authSecret = getAuthSecret();
28
- const serviceAccountJson = await decrypt(serviceAccountCiphertext, authSecret);
29
- const resolved = resolveConfigInput({
30
- siteOrigin,
31
- ga4PropertyId,
32
- gscSiteUrl,
33
- serviceAccountJson
34
- });
35
- if (!resolved.success) {
36
- throw new Error(resolved.message);
37
- }
38
- return resolved.data;
39
- }
40
-
41
- export async function saveConfig(ctx: PluginCtx, input: SavedPluginConfig): Promise<PluginConfigSummary> {
42
- const resolved = resolveConfigInput(input);
43
- if (!resolved.success) {
44
- throw new Error(resolved.message);
45
- }
46
- const parsed = resolved.data;
47
- const authSecret = getAuthSecret();
48
- const serviceAccountCiphertext = await encrypt(parsed.serviceAccountJson, authSecret);
49
-
50
- await Promise.all([
51
- ctx.kv.set(CONFIG_SITE_ORIGIN_KEY, normalizeOrigin(parsed.siteOrigin)),
52
- ctx.kv.set(CONFIG_GA4_PROPERTY_ID_KEY, parsed.ga4PropertyId),
53
- ctx.kv.set(CONFIG_GSC_SITE_URL_KEY, parsed.gscSiteUrl),
54
- ctx.kv.set(CONFIG_SERVICE_ACCOUNT_KEY, serviceAccountCiphertext)
55
- ]);
56
-
57
- return summarizeConfig(parsed);
58
- }
59
-
60
- export async function getConfigSummary(ctx: PluginCtx): Promise<PluginConfigSummary> {
61
- const config = await loadConfig(ctx);
62
- if (!config) {
63
- return {
64
- siteOrigin: "",
65
- ga4PropertyId: "",
66
- gscSiteUrl: "",
67
- hasServiceAccount: false
68
- };
69
- }
70
- return summarizeConfig(config);
71
- }
72
-
73
- export function getAuthSecret(): string {
74
- const value = process.env.EMDASH_AUTH_SECRET || process.env.AUTH_SECRET || "";
75
- if (!value) {
76
- throw new Error("EMDASH_AUTH_SECRET is required to store analytics credentials");
77
- }
78
- return value;
79
- }
80
-
81
- function summarizeConfig(config: SavedPluginConfig): PluginConfigSummary {
82
- const serviceAccount = parseServiceAccount(config.serviceAccountJson);
83
- return {
84
- siteOrigin: normalizeOrigin(config.siteOrigin),
85
- ga4PropertyId: config.ga4PropertyId,
86
- gscSiteUrl: config.gscSiteUrl,
87
- hasServiceAccount: true,
88
- serviceAccountEmail: serviceAccount.client_email
89
- };
90
- }
package/src/constants.ts DELETED
@@ -1,55 +0,0 @@
1
- export const PLUGIN_ID = "emdash-google-analytics-dashboard";
2
- export const PLUGIN_VERSION = "0.1.0";
3
- export const AGENT_KEY_PREFIX = "yb_ins_";
4
-
5
- export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
6
- export const GOOGLE_GA_BASE_URL = "https://analyticsdata.googleapis.com/v1beta";
7
- export const GOOGLE_GSC_BASE_URL = "https://www.googleapis.com/webmasters/v3";
8
-
9
- export const GSC_SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
10
- export const GA_SCOPE = "https://www.googleapis.com/auth/analytics.readonly";
11
-
12
- export const DEFAULT_PAGE_WINDOW_DAYS = 28;
13
- export const GSC_DATA_DELAY_DAYS = 3;
14
- export const QUERY_REFRESH_STALE_HOURS = 48;
15
- export const GSC_QUERY_PAGE_LIMIT = 50;
16
- export const GSC_QUERY_ROW_LIMIT = 25;
17
-
18
- export const SITE_SUMMARY_KEY = "state:site-summary";
19
- export const FRESHNESS_KEY = "state:freshness";
20
- export const CONFIG_SITE_ORIGIN_KEY = "settings:siteOrigin";
21
- export const CONFIG_GA4_PROPERTY_ID_KEY = "settings:ga4PropertyId";
22
- export const CONFIG_GSC_SITE_URL_KEY = "settings:gscSiteUrl";
23
- export const CONFIG_SERVICE_ACCOUNT_KEY = "settings:serviceAccountCiphertext";
24
-
25
- export const CRON_SYNC_BASE = "sync-base";
26
- export const CRON_ENRICH_MANAGED = "enrich-managed-queries";
27
-
28
- export const PUBLIC_AGENT_ROUTES = {
29
- SITE_SUMMARY: "agent/v1/site-summary",
30
- OPPORTUNITIES: "agent/v1/opportunities",
31
- CONTENT_CONTEXT: "agent/v1/content-context"
32
- } as const;
33
-
34
- export const ADMIN_ROUTES = {
35
- STATUS: "admin/status",
36
- OVERVIEW: "admin/overview",
37
- LIST_PAGES: "admin/pages/list",
38
- CONTENT_CONTEXT: "admin/content/get",
39
- CONFIG_GET: "admin/config/get",
40
- CONFIG_SAVE: "admin/config/save",
41
- CONNECTION_TEST: "admin/connection/test",
42
- SYNC_NOW: "admin/sync-now",
43
- AGENT_KEYS_LIST: "admin/agent-keys/list",
44
- AGENT_KEYS_CREATE: "admin/agent-keys/create",
45
- AGENT_KEYS_REVOKE: "admin/agent-keys/revoke"
46
- } as const;
47
-
48
- export const PAGE_KIND_ORDER = [
49
- "blog_post",
50
- "blog_archive",
51
- "tag",
52
- "author",
53
- "landing",
54
- "other"
55
- ] as const;
package/src/content.ts DELETED
@@ -1,133 +0,0 @@
1
- import { getEmDashCollection, getEmDashEntry } from "emdash";
2
-
3
- import type { ManagedContentRef, PageKind } from "./types.js";
4
-
5
- interface EntryLike {
6
- id?: string;
7
- slug?: string | null;
8
- data?: Record<string, unknown>;
9
- }
10
-
11
- export function classifyPageKind(urlPath: string): PageKind {
12
- if (/^\/blog\/[^/]+\/$/.test(urlPath)) return "blog_post";
13
- if (urlPath === "/blog/" || /^\/blog\/[^/]+\/$/.test(urlPath)) return "blog_archive";
14
- if (urlPath.startsWith("/tag/")) return "tag";
15
- if (urlPath.startsWith("/author/")) return "author";
16
- if (
17
- urlPath === "/" ||
18
- urlPath.startsWith("/company/") ||
19
- urlPath.startsWith("/contact/") ||
20
- urlPath.startsWith("/download/") ||
21
- urlPath.startsWith("/foreignworkers/") ||
22
- urlPath.startsWith("/jirei/") ||
23
- urlPath.startsWith("/kaigo/") ||
24
- urlPath.startsWith("/pr/") ||
25
- urlPath.startsWith("/seminar/")
26
- ) {
27
- return "landing";
28
- }
29
- return "other";
30
- }
31
-
32
- export function normalizePath(pathOrUrl: string, expectedHost?: string): string | null {
33
- try {
34
- const parsed = pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")
35
- ? new URL(pathOrUrl)
36
- : new URL(pathOrUrl, "https://placeholder.invalid");
37
- if (expectedHost && parsed.hostname !== "placeholder.invalid" && parsed.hostname !== expectedHost) {
38
- return null;
39
- }
40
- let pathname = parsed.pathname || "/";
41
- if (!pathname.startsWith("/")) pathname = `/${pathname}`;
42
- pathname = pathname.replace(/\/{2,}/g, "/");
43
- if (pathname !== "/" && !pathname.endsWith("/")) pathname = `${pathname}/`;
44
- return pathname;
45
- } catch {
46
- return null;
47
- }
48
- }
49
-
50
- export function buildContentUrl(siteOrigin: string, urlPath: string): string {
51
- return new URL(urlPath, `${siteOrigin}/`).toString();
52
- }
53
-
54
- export async function getManagedContentMap(siteOrigin: string): Promise<Map<string, ManagedContentRef>> {
55
- const result = await getEmDashCollection("posts", {
56
- status: "published",
57
- limit: 1000,
58
- orderBy: { updatedAt: "desc" }
59
- });
60
-
61
- const managed = new Map<string, ManagedContentRef>();
62
- for (const entry of result.entries as Array<EntryLike>) {
63
- const id = typeof entry.id === "string" ? entry.id : "";
64
- if (!id) continue;
65
- const slug = typeof entry.slug === "string" ? entry.slug : null;
66
- const data = entry.data ?? {};
67
- const title = typeof data.title === "string" ? data.title : slug || id;
68
- const excerpt = typeof data.excerpt === "string" ? data.excerpt : undefined;
69
- const seoDescription =
70
- typeof data.seo_description === "string" ? data.seo_description : undefined;
71
- const urlPath = `/blog/${slug || id}/`;
72
- managed.set(urlPath, {
73
- collection: "posts",
74
- id,
75
- slug,
76
- urlPath,
77
- title,
78
- excerpt,
79
- seoDescription
80
- });
81
- }
82
-
83
- void siteOrigin;
84
- return managed;
85
- }
86
-
87
- export async function resolveManagedContent(
88
- collection: string,
89
- id?: string,
90
- slug?: string,
91
- siteOrigin?: string
92
- ): Promise<ManagedContentRef | null> {
93
- if (collection !== "posts") return null;
94
- const ref = id || slug;
95
- if (!ref) return null;
96
-
97
- const result = await getEmDashEntry("posts", ref);
98
- const entry = result.entry as EntryLike | null;
99
- if (!entry) return null;
100
- const entryId = typeof entry.id === "string" ? entry.id : ref;
101
- const entrySlug = typeof entry.slug === "string" ? entry.slug : null;
102
- const data = entry.data ?? {};
103
- const urlPath = `/blog/${entrySlug || entryId}/`;
104
- return {
105
- collection: "posts",
106
- id: entryId,
107
- slug: entrySlug,
108
- urlPath,
109
- title: typeof data.title === "string" ? data.title : entrySlug || entryId,
110
- excerpt: typeof data.excerpt === "string" ? data.excerpt : undefined,
111
- seoDescription: typeof data.seo_description === "string" ? data.seo_description : undefined
112
- };
113
- }
114
-
115
- export function pageStorageId(urlPath: string): string {
116
- return stableStorageId(urlPath);
117
- }
118
-
119
- export function pageQueryStorageId(urlPath: string, query: string): string {
120
- return stableStorageId(`${urlPath}::${query}`);
121
- }
122
-
123
- export function dailyMetricStorageId(source: "gsc" | "ga", scope: "all_public", date: string): string {
124
- return stableStorageId(`${source}::${scope}::${date}`);
125
- }
126
-
127
- function stableStorageId(input: string): string {
128
- let hash = 5381;
129
- for (let index = 0; index < input.length; index += 1) {
130
- hash = (hash * 33) ^ input.charCodeAt(index);
131
- }
132
- return `ybci_${(hash >>> 0).toString(16)}`;
133
- }