@gscdump/devframe 5.7.0 → 5.7.1

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Search Console</title>
7
- <script type="module" crossorigin src="./assets/index-B8m88k2l.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-Bv6TqI10.css">
7
+ <script type="module" crossorigin src="./assets/index-DA7Lgn7_.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-BdO8MoYt.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
package/dist/index.d.mts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { DailyPoint, DateWindow, GscdumpContext, MetricTotals, PageStats, PageStatsInput, Period, QueryRow, SiteSummary } from "./shared/protocol.mjs";
2
- import { GscdumpReaderOptions } from "./reader.mjs";
2
+ import { HostedSourceOptions } from "./sources/hosted.mjs";
3
3
  import { DevframeDefinition } from "devframe";
4
- export interface GscdumpDevframeOptions extends GscdumpReaderOptions {
4
+ export interface GscdumpDevframeOptions extends HostedSourceOptions {
5
+ /** The Site to read: a Site ID, a Site URL, or its host. Optional when the credential holds one Site. */
6
+ site?: string;
5
7
  /** Override the request implementation, for proxies and tests. */
6
8
  fetch?: typeof fetch;
7
9
  }
@@ -13,4 +15,4 @@ export interface GscdumpDevframeOptions extends GscdumpReaderOptions {
13
15
  * devframe hub.
14
16
  */
15
17
  export declare function createGscdumpDevframe(options?: GscdumpDevframeOptions): DevframeDefinition;
16
- export { type DailyPoint, type DateWindow, type GscdumpContext, type GscdumpReaderOptions, type MetricTotals, type PageStats, type PageStatsInput, type Period, type QueryRow, type SiteSummary, createGscdumpDevframe as default };
18
+ export { type DailyPoint, type DateWindow, type GscdumpContext, type HostedSourceOptions, type MetricTotals, type PageStats, type PageStatsInput, type Period, type QueryRow, type SiteSummary, createGscdumpDevframe as default };
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { description, homepage, name, version } from "./package.mjs";
2
2
  import { DEVFRAME_ID } from "./shared/protocol.mjs";
3
- import { createGscdumpReader } from "./reader.mjs";
3
+ import { createPageStatsReader } from "./reader.mjs";
4
4
  import { createRpcFunctions } from "./rpc.mjs";
5
+ import { createHostedSource } from "./sources/hosted.mjs";
5
6
  import { createRequire } from "node:module";
6
7
  import { readFile } from "node:fs/promises";
7
8
  import { homedir } from "node:os";
@@ -38,12 +39,12 @@ function createGscdumpDevframe(options = {}) {
38
39
  capabilities: { build: false },
39
40
  setup(ctx) {
40
41
  const env = process.env;
41
- const reader = createGscdumpReader(options, {
42
+ const source = createHostedSource(options, {
42
43
  fetch: options.fetch ?? globalThis.fetch,
43
44
  env,
44
- readCliAuthentication: () => readCliAuthentication(env),
45
- now: Date.now
45
+ readCliAuthentication: () => readCliAuthentication(env)
46
46
  });
47
+ const reader = createPageStatsReader({ site: options.site }, source, Date.now);
47
48
  const scoped = ctx.scope(DEVFRAME_ID);
48
49
  for (const fn of createRpcFunctions(reader)) scoped.rpc.register(fn);
49
50
  }
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  var name = "@gscdump/devframe";
2
- var version = "5.7.0";
2
+ var version = "5.7.1";
3
3
  var description = "Search Console clicks, impressions, and queries for the page you are developing, in Vite DevTools and any devframe hub.";
4
4
  var homepage = "https://github.com/harlan-zw/gscdump/tree/main/packages/devframe#readme";
5
5
  export { description, homepage, name, version };
package/dist/reader.mjs CHANGED
@@ -1,190 +1,172 @@
1
- import { bearerOf, dashboardOrigin, resolveCredential } from "./credential.mjs";
2
- import { matchSite, readAccount } from "./account.mjs";
1
+ import { hostOf } from "./source.mjs";
2
+ import { matchSite } from "./account.mjs";
3
3
  import { addDays, dashboardPageUrl, pagePathOf, parseQueryRows, periodWindows, summariseDaily } from "./page-stats.mjs";
4
4
  import { PERIODS } from "./shared/protocol.mjs";
5
- import { parseRecordReadRefusal } from "@gscdump/contracts";
6
5
  import { periodToDateRange } from "@gscdump/sdk/period";
7
- import { createGscdumpV1Client, isGscdumpV1Error } from "@gscdump/sdk/v1";
8
- const ACCOUNT_TTL_MS = 3e5;
6
+ const SITES_TTL_MS = 3e5;
9
7
  const STATS_TTL_MS = 3e5;
10
8
  const TOP_QUERIES = 25;
11
- function rowsBody(dimension, path, window) {
12
- return {
13
- dimensions: [dimension],
14
- filter: { _filters: [{
15
- dimension: "date",
16
- operator: "between",
17
- expression: window.start,
18
- expression2: window.end
19
- }, {
20
- dimension: "page",
21
- operator: "equals",
22
- expression: path
23
- }] },
24
- orderBy: dimension === "date" ? {
25
- column: "date",
26
- dir: "asc"
27
- } : {
28
- column: "clicks",
29
- dir: "desc"
30
- },
31
- rowLimit: dimension === "date" ? 400 : TOP_QUERIES
32
- };
33
- }
34
9
  function periodDays(period) {
35
10
  return PERIODS.find((option) => option.value === period)?.days ?? 28;
36
11
  }
37
- function createGscdumpReader(options, deps) {
38
- let session = null;
12
+ function contextForPage(sites, pageUrl, preferredSiteId) {
13
+ const host = hostOf(pageUrl) ?? pageUrl;
14
+ const matches = sites.filter((site) => site.host === host);
15
+ if (matches.length === 0) return {
16
+ _tag: "NoSiteForPage",
17
+ host
18
+ };
19
+ const pick = matches.find((site) => site.siteId === preferredSiteId) ?? (matches.length === 1 ? matches[0] : void 0);
20
+ return pick ? {
21
+ _tag: "Ready",
22
+ site: pick,
23
+ sites: matches,
24
+ configured: false
25
+ } : {
26
+ _tag: "SiteRequired",
27
+ sites: matches,
28
+ target: host
29
+ };
30
+ }
31
+ function createPageStatsReader(options, source, now) {
32
+ let sitesCache = null;
39
33
  const stats = /* @__PURE__ */ new Map();
40
- async function loadSession() {
41
- const credential = await resolveCredential({
42
- apiKey: options.apiKey,
43
- apiRoot: options.apiRoot,
44
- env: deps.env,
45
- readCliAuthentication: deps.readCliAuthentication
46
- });
47
- if (!credential) return {
48
- _tag: "Blocked",
49
- context: { _tag: "CredentialMissing" }
50
- };
51
- const account = await readAccount(credential, deps.fetch);
52
- if (account._tag === "Rejected") return {
53
- _tag: "Blocked",
54
- context: {
55
- _tag: "CredentialRejected",
56
- source: credential.source
57
- }
58
- };
59
- if (account._tag === "Unavailable") return {
60
- _tag: "Blocked",
61
- context: {
62
- _tag: "Unavailable",
63
- message: account.message
64
- }
65
- };
66
- if (account.sites.length === 0) return {
67
- _tag: "Blocked",
68
- context: { _tag: "NoSites" }
69
- };
70
- return {
71
- _tag: "Ready",
72
- credential,
73
- client: createGscdumpV1Client({
74
- apiRoot: credential.apiRoot,
75
- credential: bearerOf(credential),
76
- fetch: deps.fetch
77
- }),
78
- sites: account.sites
79
- };
80
- }
81
- function currentSession() {
82
- if (session && deps.now() - session.at < ACCOUNT_TTL_MS) return session.value;
83
- const value = loadSession();
84
- session = {
85
- at: deps.now(),
34
+ function readSites() {
35
+ if (sitesCache && now() - sitesCache.at < SITES_TTL_MS) return sitesCache.value;
36
+ const value = source.sites();
37
+ sitesCache = {
38
+ at: now(),
86
39
  value
87
40
  };
88
41
  value.then((result) => {
89
- if (result._tag === "Blocked" && session?.value === value) session = null;
42
+ if (result._tag === "Blocked" && sitesCache?.value === value) sitesCache = null;
90
43
  }, () => {
91
- if (session?.value === value) session = null;
44
+ if (sitesCache?.value === value) sitesCache = null;
92
45
  });
93
46
  return value;
94
47
  }
95
- async function context(preferredSiteId) {
96
- const current = await currentSession();
97
- if (current._tag === "Blocked") return current.context;
48
+ async function context(preferredSiteId, pageUrl) {
49
+ const read = await readSites();
50
+ if (read._tag === "Blocked") return read.context;
51
+ const sites = read.sites;
52
+ if (sites.length === 0) return { _tag: "NoSites" };
98
53
  if (options.site) {
99
- const match = matchSite(current.sites, options.site);
100
- return match._tag === "Found" ? {
54
+ const match = matchSite(sites, options.site);
55
+ if (match._tag === "Found") return {
101
56
  _tag: "Ready",
102
57
  site: match.site,
103
- sites: current.sites,
58
+ sites,
104
59
  configured: true
60
+ };
61
+ const picked = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
62
+ return picked ? {
63
+ _tag: "Ready",
64
+ site: picked,
65
+ sites,
66
+ configured: false
105
67
  } : {
106
68
  _tag: "SiteRequired",
107
- sites: current.sites,
69
+ sites,
108
70
  target: options.site
109
71
  };
110
72
  }
111
- const preferred = preferredSiteId ? current.sites.find((site) => site.siteId === preferredSiteId) : void 0;
73
+ if (options.siteFromPage && pageUrl) return contextForPage(sites, pageUrl, preferredSiteId ?? null);
74
+ const preferred = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
112
75
  if (preferred) return {
113
76
  _tag: "Ready",
114
77
  site: preferred,
115
- sites: current.sites,
78
+ sites,
116
79
  configured: false
117
80
  };
118
- const match = matchSite(current.sites, null);
81
+ const match = matchSite(sites, null);
119
82
  return match._tag === "Found" ? {
120
83
  _tag: "Ready",
121
84
  site: match.site,
122
- sites: current.sites,
85
+ sites,
123
86
  configured: false
124
87
  } : {
125
88
  _tag: "SiteRequired",
126
- sites: current.sites,
89
+ sites,
90
+ target: null
91
+ };
92
+ }
93
+ async function siteForPage(input) {
94
+ if (!options.siteFromPage) return context(input.siteId);
95
+ const read = await readSites();
96
+ if (read._tag === "Blocked") return read.context;
97
+ const chosen = input.siteId ? read.sites.find((site) => site.siteId === input.siteId) : void 0;
98
+ if (chosen) return {
99
+ _tag: "Ready",
100
+ site: chosen,
101
+ sites: read.sites,
102
+ configured: false
103
+ };
104
+ return /^https?:\/\//i.test(input.page) ? contextForPage(read.sites, input.page, null) : {
105
+ _tag: "SiteRequired",
106
+ sites: read.sites,
127
107
  target: null
128
108
  };
129
109
  }
130
- async function readRows(client, siteId, body) {
131
- return (await client.queryAnalyticsRows({
132
- params: { siteId },
133
- body
134
- })).data.rows;
110
+ function firstFailure(daily, queries) {
111
+ if (daily._tag !== "Ok") return daily;
112
+ return queries._tag !== "Ok" ? queries : null;
135
113
  }
136
- async function readPage(current, site, path, period, windows, retried) {
114
+ async function readPage(site, path, period, windows, retried) {
137
115
  const span = {
138
116
  start: windows.previous?.start ?? windows.current.start,
139
117
  end: windows.current.end
140
118
  };
141
- const result = await Promise.all([readRows(current.client, site.siteId, rowsBody("date", path, span)), readRows(current.client, site.siteId, rowsBody("query", path, windows.current))]).catch((error) => error);
142
- if (Array.isArray(result)) {
143
- const [daily, queries] = result;
144
- return {
145
- _tag: "Ok",
146
- siteId: site.siteId,
147
- path,
148
- period,
149
- window: windows.current,
150
- previousWindow: windows.previous,
151
- ...summariseDaily(daily, windows),
152
- queries: parseQueryRows(queries),
153
- dashboardUrl: dashboardPageUrl(dashboardOrigin(current.credential.apiRoot), site, path)
154
- };
155
- }
156
- if (!isGscdumpV1Error(result)) throw result;
157
- const refusal = parseRecordReadRefusal(result.details);
158
- if (refusal?.reason === "range_not_synced" && !retried && refusal.missingStart > windows.current.start) {
159
- const shifted = periodWindows(periodDays(period), {
160
- oldestDate: site.oldestDate,
161
- newestDate: addDays(refusal.missingStart, -1)
162
- });
163
- if (shifted) return readPage(current, site, path, period, shifted, true);
164
- }
165
- if (refusal?.reason === "record_not_ready") return {
166
- _tag: "NoData",
119
+ const [daily, queries] = await Promise.all([source.dailyRows(site.siteId, path, span), source.queryRows(site.siteId, path, windows.current, TOP_QUERIES)]);
120
+ const failure = firstFailure(daily, queries);
121
+ if (!failure && daily._tag === "Ok" && queries._tag === "Ok") return {
122
+ _tag: "Ok",
167
123
  siteId: site.siteId,
168
- message: "gscdump has not prepared this Site's record for reads yet. Try again in a few minutes."
169
- };
170
- if (refusal?.reason === "range_not_synced") return {
171
- _tag: "NoData",
172
- siteId: site.siteId,
173
- message: `The Site's record does not hold ${refusal.missingStart} to ${refusal.missingEnd} yet.`
174
- };
175
- if (result.code === "rate_limited") return { _tag: "RateLimited" };
176
- if (result.status === 401 || result.status === 403) return {
177
- _tag: "SiteUnavailable",
178
- context: {
179
- _tag: "CredentialRejected",
180
- source: current.credential.source
124
+ path,
125
+ period,
126
+ window: windows.current,
127
+ previousWindow: windows.previous,
128
+ ...summariseDaily(daily.rows, windows),
129
+ queries: parseQueryRows(queries.rows),
130
+ dashboardUrl: dashboardPageUrl(source.dashboardOrigin, site, path)
131
+ };
132
+ switch (failure?._tag) {
133
+ case "Refused": {
134
+ const refusal = failure.refusal;
135
+ if (refusal.reason === "range_not_synced" && !retried && refusal.missingStart > windows.current.start) {
136
+ const shifted = periodWindows(periodDays(period), {
137
+ oldestDate: site.oldestDate,
138
+ newestDate: addDays(refusal.missingStart, -1)
139
+ });
140
+ if (shifted) return readPage(site, path, period, shifted, true);
141
+ }
142
+ return refusal.reason === "record_not_ready" ? {
143
+ _tag: "NoData",
144
+ siteId: site.siteId,
145
+ message: "gscdump has not prepared this Site's record for reads yet. Try again in a few minutes."
146
+ } : {
147
+ _tag: "NoData",
148
+ siteId: site.siteId,
149
+ message: `The Site's record does not hold ${refusal.missingStart} to ${refusal.missingEnd} yet.`
150
+ };
181
151
  }
182
- };
183
- return {
184
- _tag: "Failed",
185
- message: result.message,
186
- requestId: result.requestId ?? null
187
- };
152
+ case "RateLimited": return { _tag: "RateLimited" };
153
+ case "Unauthorized":
154
+ sitesCache = null;
155
+ return {
156
+ _tag: "SiteUnavailable",
157
+ context: failure.context
158
+ };
159
+ case "Failed": return {
160
+ _tag: "Failed",
161
+ message: failure.message,
162
+ requestId: failure.requestId
163
+ };
164
+ default: return {
165
+ _tag: "Failed",
166
+ message: "The read returned no rows and no error.",
167
+ requestId: null
168
+ };
169
+ }
188
170
  }
189
171
  async function pageStats(input) {
190
172
  const path = pagePathOf(input.page);
@@ -193,7 +175,7 @@ function createGscdumpReader(options, deps) {
193
175
  page: input.page
194
176
  };
195
177
  const period = input.period ?? "28d";
196
- const resolved = await context(input.siteId);
178
+ const resolved = await siteForPage(input);
197
179
  if (resolved._tag !== "Ready") return {
198
180
  _tag: "SiteUnavailable",
199
181
  context: resolved
@@ -201,13 +183,8 @@ function createGscdumpReader(options, deps) {
201
183
  const site = resolved.site;
202
184
  const key = `${site.siteId}|${period}|${path}`;
203
185
  const hit = stats.get(key);
204
- if (hit && deps.now() - hit.at < STATS_TTL_MS) return hit.value;
205
- const current = await currentSession();
206
- if (current._tag !== "Ready") return {
207
- _tag: "SiteUnavailable",
208
- context: current.context
209
- };
210
- const stableEnd = periodToDateRange(period, { now: new Date(deps.now()) }).end;
186
+ if (hit && now() - hit.at < STATS_TTL_MS) return hit.value;
187
+ const stableEnd = periodToDateRange(period, { now: new Date(now()) }).end;
211
188
  const newestDate = site.newestDate && site.newestDate > stableEnd ? stableEnd : site.newestDate;
212
189
  const windows = periodWindows(periodDays(period), {
213
190
  oldestDate: site.oldestDate,
@@ -218,9 +195,9 @@ function createGscdumpReader(options, deps) {
218
195
  siteId: site.siteId,
219
196
  message: "The Site's record holds no days yet. gscdump is still syncing it."
220
197
  };
221
- const value = readPage(current, site, path, period, windows, false);
198
+ const value = readPage(site, path, period, windows, false);
222
199
  stats.set(key, {
223
- at: deps.now(),
200
+ at: now(),
224
201
  value
225
202
  });
226
203
  value.then((result) => {
@@ -235,4 +212,4 @@ function createGscdumpReader(options, deps) {
235
212
  pageStats
236
213
  };
237
214
  }
238
- export { createGscdumpReader };
215
+ export { createPageStatsReader };
package/dist/rpc.mjs CHANGED
@@ -15,7 +15,7 @@ function createRpcFunctions(reader) {
15
15
  name: "get-context",
16
16
  type: "query",
17
17
  jsonSerializable: true,
18
- handler: (preferredSiteId) => reader.context(preferredSiteId ?? null)
18
+ handler: (preferredSiteId, pageUrl) => reader.context(preferredSiteId ?? null, pageUrl ?? null)
19
19
  }), defineRpcFunction({
20
20
  name: "get-page-stats",
21
21
  type: "query",
@@ -3,14 +3,16 @@ export interface SiteSummary {
3
3
  siteId: string;
4
4
  /** The Site as gscdump registered it, for example `example.com`. */
5
5
  siteUrl: string;
6
+ /** The exact host the Site covers, for example `example.com`. A page on another host is not on this Site. */
7
+ host: string;
6
8
  /** The first day the Hosted record holds, `YYYY-MM-DD`. */
7
9
  oldestDate: string | null;
8
10
  /** The last day the Hosted record holds, `YYYY-MM-DD`. */
9
11
  newestDate: string | null;
10
12
  }
11
13
  /**
12
- * What the node side can read, decided once per panel boot. Each tag names
13
- * the next step the panel shows.
14
+ * What the panel can read, decided once per panel boot and again when the
15
+ * page's host changes. Each tag names the next step the panel shows.
14
16
  */
15
17
  export type GscdumpContext = {
16
18
  _tag: 'Ready';
@@ -22,12 +24,20 @@ export type GscdumpContext = {
22
24
  sites: SiteSummary[];
23
25
  target: string | null;
24
26
  } | {
27
+ _tag: 'NoSiteForPage';
28
+ host: string;
29
+ } | BlockedContext;
30
+ /** A context that stops before any Site is chosen: no credential, no Sites, or no service. */
31
+ export type BlockedContext = {
25
32
  _tag: 'NoSites';
26
33
  } | {
27
34
  _tag: 'CredentialMissing';
28
35
  } | {
29
36
  _tag: 'CredentialRejected';
30
37
  source: CredentialSource;
38
+ } | {
39
+ _tag: 'SignedOut';
40
+ signInUrl: string;
31
41
  } | {
32
42
  _tag: 'Unavailable';
33
43
  message: string;
@@ -0,0 +1 @@
1
+ import "./shared/protocol.mjs";
@@ -0,0 +1,7 @@
1
+ function hostOf(value) {
2
+ const text = value.trim();
3
+ if (text.startsWith("sc-domain:")) return text.slice(10).toLowerCase().replace(/\.$/, "") || null;
4
+ const url = URL.parse(/^[a-z][a-z\d+.-]*:\/\//i.test(text) ? text : `https://${text}`);
5
+ return url?.hostname ? url.hostname.toLowerCase() : null;
6
+ }
7
+ export { hostOf };
@@ -1,7 +1,5 @@
1
- import "./shared/protocol.mjs";
2
- export interface GscdumpReaderOptions {
3
- /** The Site to read: a Site ID, a Site URL, or its host. Optional when the credential holds one Site. */
4
- site?: string;
1
+ import "../source.mjs";
2
+ export interface HostedSourceOptions {
5
3
  /** A gscdump user API key. Defaults to `GSCDUMP_API_KEY`, then the CLI's Hosted login. */
6
4
  apiKey?: string;
7
5
  /** Defaults to `GSCDUMP_API_ROOT`, then `https://gscdump.com/api`. */
@@ -0,0 +1,118 @@
1
+ import { bearerOf, dashboardOrigin, resolveCredential } from "../credential.mjs";
2
+ import { readAccount } from "../account.mjs";
3
+ import { parseRecordReadRefusal } from "@gscdump/contracts";
4
+ import { createGscdumpV1Client, isGscdumpV1Error } from "@gscdump/sdk/v1";
5
+ function rowsBody(dimension, path, window, rowLimit) {
6
+ return {
7
+ dimensions: [dimension],
8
+ filter: { _filters: [{
9
+ dimension: "date",
10
+ operator: "between",
11
+ expression: window.start,
12
+ expression2: window.end
13
+ }, {
14
+ dimension: "page",
15
+ operator: "equals",
16
+ expression: path
17
+ }] },
18
+ orderBy: dimension === "date" ? {
19
+ column: "date",
20
+ dir: "asc"
21
+ } : {
22
+ column: "clicks",
23
+ dir: "desc"
24
+ },
25
+ rowLimit
26
+ };
27
+ }
28
+ function createHostedSource(options, deps) {
29
+ const apiRoot = (options.apiRoot ?? deps.env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api").replace(/\/+$/, "");
30
+ let ready = null;
31
+ async function sites() {
32
+ const read = await readSites();
33
+ if (read._tag === "Blocked") ready = null;
34
+ return read;
35
+ }
36
+ async function readSites() {
37
+ const credential = await resolveCredential({
38
+ apiKey: options.apiKey,
39
+ apiRoot: options.apiRoot,
40
+ env: deps.env,
41
+ readCliAuthentication: deps.readCliAuthentication
42
+ });
43
+ if (!credential) return {
44
+ _tag: "Blocked",
45
+ context: { _tag: "CredentialMissing" }
46
+ };
47
+ const account = await readAccount(credential, deps.fetch);
48
+ if (account._tag === "Rejected") return {
49
+ _tag: "Blocked",
50
+ context: {
51
+ _tag: "CredentialRejected",
52
+ source: credential.source
53
+ }
54
+ };
55
+ if (account._tag === "Unavailable") return {
56
+ _tag: "Blocked",
57
+ context: {
58
+ _tag: "Unavailable",
59
+ message: account.message
60
+ }
61
+ };
62
+ ready = {
63
+ credential,
64
+ client: createGscdumpV1Client({
65
+ apiRoot: credential.apiRoot,
66
+ credential: bearerOf(credential),
67
+ fetch: deps.fetch
68
+ })
69
+ };
70
+ return {
71
+ _tag: "Ok",
72
+ sites: account.sites
73
+ };
74
+ }
75
+ async function readRows(siteId, body) {
76
+ if (!ready) return {
77
+ _tag: "Unauthorized",
78
+ context: { _tag: "CredentialMissing" }
79
+ };
80
+ const { client, credential } = ready;
81
+ return client.queryAnalyticsRows({
82
+ params: { siteId },
83
+ body
84
+ }).then((response) => ({
85
+ _tag: "Ok",
86
+ rows: response.data.rows
87
+ }), (error) => {
88
+ if (!isGscdumpV1Error(error)) throw error;
89
+ const refusal = parseRecordReadRefusal(error.details);
90
+ if (refusal) return {
91
+ _tag: "Refused",
92
+ refusal
93
+ };
94
+ if (error.code === "rate_limited") return { _tag: "RateLimited" };
95
+ if (error.status === 401 || error.status === 403) return {
96
+ _tag: "Unauthorized",
97
+ context: {
98
+ _tag: "CredentialRejected",
99
+ source: credential.source
100
+ }
101
+ };
102
+ return {
103
+ _tag: "Failed",
104
+ message: error.message,
105
+ requestId: error.requestId ?? null
106
+ };
107
+ });
108
+ }
109
+ return {
110
+ sites,
111
+ dailyRows: (siteId, path, window) => readRows(siteId, rowsBody("date", path, window, 400)),
112
+ queryRows: (siteId, path, window, limit) => readRows(siteId, rowsBody("query", path, window, limit)),
113
+ get dashboardOrigin() {
114
+ return dashboardOrigin(ready?.credential.apiRoot ?? apiRoot);
115
+ }
116
+ };
117
+ }
118
+ export { createHostedSource };