@domandigital/gbp 0.3.1 → 0.4.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/CHANGELOG.md +43 -1
- package/README.md +88 -15
- package/dist/index.cjs +309 -39
- package/dist/index.d.cts +220 -4
- package/dist/index.d.ts +220 -4
- package/dist/index.js +298 -38
- package/package.json +17 -15
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that decides how long to wait for Google and when to try again.
|
|
3
|
+
*
|
|
4
|
+
* Every request gets a deadline, because a stalled response would otherwise
|
|
5
|
+
* hold a Next.js render or build until the platform kills it. Rate limits
|
|
6
|
+
* (429) and transient server errors (500, 502, 503, 504) get a small, bounded
|
|
7
|
+
* number of retries with full-jitter backoff. Google documents the 429 for
|
|
8
|
+
* quota (developers.google.com/my-business/content/limits); whether the
|
|
9
|
+
* Business Profile API sends `Retry-After` is not documented, so it is honoured
|
|
10
|
+
* when present and not relied on.
|
|
11
|
+
*
|
|
12
|
+
* Nothing else is retried: a 400, 403 or 404 needs a person to change
|
|
13
|
+
* something, and repeating the request only delays the error that says so.
|
|
14
|
+
*/
|
|
15
|
+
interface GbpRequestOptions {
|
|
16
|
+
/** Deadline for each attempt, in ms. Default 8000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Attempts per request, counting the first. Default 3. */
|
|
19
|
+
maxAttempts?: number;
|
|
20
|
+
/** Base of the exponential backoff, in ms. Default 250. */
|
|
21
|
+
baseDelayMs?: number;
|
|
22
|
+
/** Cap on a single backoff, in ms. Default 5000. */
|
|
23
|
+
maxDelayMs?: number;
|
|
24
|
+
/** Longest `Retry-After` this will sleep through, in ms. Longer, and the error is returned instead. Default 30000. */
|
|
25
|
+
maxRetryAfterMs?: number;
|
|
26
|
+
/** Cancels everything, including a wait between attempts. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
declare const DEFAULT_REQUEST_POLICY: {
|
|
30
|
+
readonly timeoutMs: 8000;
|
|
31
|
+
readonly maxAttempts: 3;
|
|
32
|
+
readonly baseDelayMs: 250;
|
|
33
|
+
readonly maxDelayMs: 5000;
|
|
34
|
+
readonly maxRetryAfterMs: 30000;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* `Retry-After` as milliseconds: either delay-seconds or an HTTP-date (RFC
|
|
38
|
+
* 9110 section 10.2.3). `null` when absent or unreadable.
|
|
39
|
+
*/
|
|
40
|
+
declare function parseRetryAfter(value: string | null | undefined, nowMs?: number): number | null;
|
|
41
|
+
|
|
1
42
|
/**
|
|
2
43
|
* Google user OAuth (refresh-token) auth -- for APIs that reject service
|
|
3
44
|
* accounts, notably the Business Profile API (all scopes) and Search Console
|
|
@@ -9,17 +50,39 @@
|
|
|
9
50
|
* GBP_REFRESH_TOKEN values are set in the deploying app's environment -- not
|
|
10
51
|
* a code-level distinction. Each app/deployment configures its own.
|
|
11
52
|
*
|
|
53
|
+
* The access token is cached per process until a minute before it expires.
|
|
54
|
+
* Concurrent callers that miss the cache share one refresh rather than each
|
|
55
|
+
* starting their own. A caller that is told the token is no longer good (a
|
|
56
|
+
* 401 from the API) invalidates exactly that token and forces one refresh;
|
|
57
|
+
* see `invalidateAccessToken`.
|
|
58
|
+
*
|
|
12
59
|
* Env: GBP_CLIENT_ID, GBP_CLIENT_SECRET, GBP_REFRESH_TOKEN.
|
|
13
60
|
*/
|
|
61
|
+
|
|
14
62
|
/** True when the OAuth client + refresh token are present. */
|
|
15
63
|
declare function hasGoogleOAuthCredentials(): boolean;
|
|
64
|
+
interface AccessTokenOptions {
|
|
65
|
+
/** Ignore the cache and refresh now, e.g. after the API rejected the cached token. */
|
|
66
|
+
forceRefresh?: boolean;
|
|
67
|
+
/** Deadline and retry policy for the token request. */
|
|
68
|
+
request?: GbpRequestOptions;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Drop the cached token, but only if it is still the one the caller used. A
|
|
72
|
+
* request that was rejected with an old token must not throw away a newer one
|
|
73
|
+
* another request has already fetched.
|
|
74
|
+
*/
|
|
75
|
+
declare function invalidateAccessToken(accessToken: string): void;
|
|
16
76
|
/**
|
|
17
77
|
* Exchange the stored refresh token for a short-lived access token. Returns
|
|
18
78
|
* `null` when credentials aren't configured so callers can degrade
|
|
19
79
|
* gracefully. The token's actual scopes are whatever the refresh token was
|
|
20
80
|
* granted with -- not scoped per call.
|
|
81
|
+
*
|
|
82
|
+
* Throws `GbpAuthError`. On `invalid_grant` the cache is cleared and
|
|
83
|
+
* `reauthorizationRequired` is true: retrying cannot fix it.
|
|
21
84
|
*/
|
|
22
|
-
declare function getGoogleOAuthAccessToken(): Promise<string | null>;
|
|
85
|
+
declare function getGoogleOAuthAccessToken(options?: AccessTokenOptions): Promise<string | null>;
|
|
23
86
|
|
|
24
87
|
/**
|
|
25
88
|
* Google Business Profile -- fetch the business's own reviews, including the
|
|
@@ -43,10 +106,28 @@ declare function getGoogleOAuthAccessToken(): Promise<string | null>;
|
|
|
43
106
|
* Env: GBP_CLIENT_ID, GBP_CLIENT_SECRET, GBP_REFRESH_TOKEN,
|
|
44
107
|
* GOOGLE_BUSINESS_ACCOUNT_ID, GOOGLE_BUSINESS_LOCATION_ID.
|
|
45
108
|
*/
|
|
109
|
+
|
|
110
|
+
/** 50 pages of 50 is 2,500 reviews: far past any client's profile, well short of a runaway loop. */
|
|
111
|
+
declare const DEFAULT_MAX_PAGES = 50;
|
|
112
|
+
/** Google's moderation state for an owner reply. Replies from before moderation existed carry none. */
|
|
113
|
+
type ReviewReplyState = "PENDING" | "REJECTED" | "APPROVED";
|
|
46
114
|
interface ReviewReply {
|
|
47
115
|
text: string;
|
|
48
116
|
/** ISO 8601, when the reply was last posted/edited. */
|
|
49
117
|
updatedAt: string;
|
|
118
|
+
/** Moderation state, when Google reports one. */
|
|
119
|
+
state?: ReviewReplyState;
|
|
120
|
+
/** Why Google rejected the reply. Only set when `state` is `"REJECTED"`. New values may appear. */
|
|
121
|
+
policyViolation?: string;
|
|
122
|
+
}
|
|
123
|
+
/** A photo or video the reviewer attached. */
|
|
124
|
+
interface ReviewMedia {
|
|
125
|
+
/** The photo, or the video's thumbnail. */
|
|
126
|
+
thumbnailUrl: string;
|
|
127
|
+
/** The reviewer's own label for it, if any. */
|
|
128
|
+
label?: string;
|
|
129
|
+
/** Set for a video. */
|
|
130
|
+
videoUrl?: string;
|
|
50
131
|
}
|
|
51
132
|
interface BusinessReview {
|
|
52
133
|
id: string;
|
|
@@ -56,16 +137,61 @@ interface BusinessReview {
|
|
|
56
137
|
comment: string;
|
|
57
138
|
createdAt: string;
|
|
58
139
|
reply?: ReviewReply;
|
|
140
|
+
/** Photos and videos attached to the review, in Google's order. */
|
|
141
|
+
media?: ReviewMedia[];
|
|
142
|
+
/** Google's URL for replying to this review, for an owner-facing surface. */
|
|
143
|
+
replyUrl?: string;
|
|
59
144
|
}
|
|
60
145
|
interface BusinessReviewsResult {
|
|
146
|
+
/**
|
|
147
|
+
* Google's average for every review on the profile, rating-only ones
|
|
148
|
+
* included. `null` when Google did not report one (or the package is
|
|
149
|
+
* unconfigured): never computed from the returned list.
|
|
150
|
+
*/
|
|
61
151
|
averageRating: number | null;
|
|
62
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Google's count of every review on the profile. `null` when Google did not
|
|
154
|
+
* report one: never the length of the returned (filtered, limited) list,
|
|
155
|
+
* which would publish a wrong number into anything built from it.
|
|
156
|
+
*/
|
|
157
|
+
totalReviewCount: number | null;
|
|
63
158
|
reviews: BusinessReview[];
|
|
64
159
|
}
|
|
65
160
|
/** True when account + location + OAuth credentials are all configured. */
|
|
66
161
|
declare function isBusinessProfileConfigured(): boolean;
|
|
162
|
+
interface RawReview {
|
|
163
|
+
reviewId: string;
|
|
164
|
+
reviewer?: {
|
|
165
|
+
displayName?: string;
|
|
166
|
+
profilePhotoUrl?: string;
|
|
167
|
+
};
|
|
168
|
+
starRating?: string;
|
|
169
|
+
comment?: string;
|
|
170
|
+
createTime?: string;
|
|
171
|
+
reviewReply?: {
|
|
172
|
+
comment?: string;
|
|
173
|
+
updateTime?: string;
|
|
174
|
+
reviewReplyState?: string;
|
|
175
|
+
policyViolation?: string;
|
|
176
|
+
};
|
|
177
|
+
reviewMediaItems?: {
|
|
178
|
+
thumbnailUrl?: string;
|
|
179
|
+
thumbnailLabel?: string;
|
|
180
|
+
videoUrl?: string;
|
|
181
|
+
}[];
|
|
182
|
+
reviewReplyUrl?: string;
|
|
183
|
+
}
|
|
184
|
+
interface ReviewsPage {
|
|
185
|
+
reviews: RawReview[];
|
|
186
|
+
averageRating?: number;
|
|
187
|
+
totalReviewCount?: number;
|
|
188
|
+
nextPageToken?: string;
|
|
189
|
+
}
|
|
190
|
+
type ReviewOrder = "shuffle" | "api";
|
|
67
191
|
interface GetBusinessReviewsOptions {
|
|
68
|
-
/** Max reviews to return (default: all of them, paginated).
|
|
192
|
+
/** Max reviews to return (default: all of them, paginated). Counts usable
|
|
193
|
+
* reviews only -- see `filterMinStars` -- so this is "give me N reviews you
|
|
194
|
+
* can show", not "give me N raw API results". */
|
|
69
195
|
limit?: number;
|
|
70
196
|
/**
|
|
71
197
|
* When set, only reviews at or above this star rating are returned -- for
|
|
@@ -80,12 +206,102 @@ interface GetBusinessReviewsOptions {
|
|
|
80
206
|
revalidate?: number;
|
|
81
207
|
tags?: string[];
|
|
82
208
|
};
|
|
209
|
+
/**
|
|
210
|
+
* `"shuffle"` (default): randomizes the returned order, applied after
|
|
211
|
+
* `limit`. `"api"`: preserves the Business Profile API's own ordering
|
|
212
|
+
* (`updateTime desc`, most recent first).
|
|
213
|
+
*/
|
|
214
|
+
order?: ReviewOrder;
|
|
215
|
+
/**
|
|
216
|
+
* Keep owner replies Google has not approved (`PENDING` or `REJECTED`).
|
|
217
|
+
* Default `false`: a public page shows only replies Google shows, so a
|
|
218
|
+
* rejected reply never appears on the site as if it were live. Set `true`
|
|
219
|
+
* for an owner-facing surface, where `reply.state` and
|
|
220
|
+
* `reply.policyViolation` say what happened.
|
|
221
|
+
*/
|
|
222
|
+
includeUnapprovedReplies?: boolean;
|
|
223
|
+
/** Stop after this many pages rather than loop without end. Default 50. */
|
|
224
|
+
maxPages?: number;
|
|
225
|
+
/** Deadline and retry policy for every request this call makes. */
|
|
226
|
+
request?: GbpRequestOptions;
|
|
83
227
|
}
|
|
228
|
+
/** Validate a page of the reviews.list response. A malformed review is skipped; a malformed page throws. */
|
|
229
|
+
declare function parseListReviewsResponse(body: unknown): ReviewsPage;
|
|
84
230
|
/**
|
|
85
231
|
* Fetch reviews for the configured location, paginating through all pages
|
|
86
232
|
* (GBP caps each page at 50). Returns empty results (never throws on missing
|
|
87
233
|
* config) so UI can render unconditionally.
|
|
234
|
+
*
|
|
235
|
+
* Every request has a deadline and retries 429 and transient 5xx a bounded
|
|
236
|
+
* number of times. A 401 means the cached access token is no longer good: it
|
|
237
|
+
* is dropped, refreshed once, and the same page is asked for again. A second
|
|
238
|
+
* 401 throws. Throws `GbpApiError`, `GbpAuthError`, `GbpTimeoutError` or
|
|
239
|
+
* `GbpPaginationError`; catch and degrade in the calling route.
|
|
88
240
|
*/
|
|
89
241
|
declare function getBusinessReviews(options?: GetBusinessReviewsOptions): Promise<BusinessReviewsResult>;
|
|
90
242
|
|
|
91
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Typed failures, so a site can tell "reauthorise the Google account" from
|
|
245
|
+
* "Google is having a bad minute" from "our code looped", and log the
|
|
246
|
+
* difference without ever logging a credential.
|
|
247
|
+
*
|
|
248
|
+
* No message here carries a client id, client secret, refresh token, access
|
|
249
|
+
* token or review text. Bodies from Google are cut to a short, bounded
|
|
250
|
+
* excerpt.
|
|
251
|
+
*/
|
|
252
|
+
/** Base class: `instanceof GbpError` catches every failure this package throws. */
|
|
253
|
+
declare class GbpError extends Error {
|
|
254
|
+
constructor(message: string);
|
|
255
|
+
}
|
|
256
|
+
type GbpAuthErrorCode = "invalid_grant" | "invalid_client" | "token_request_failed";
|
|
257
|
+
/**
|
|
258
|
+
* The refresh-token exchange failed. `reauthorizationRequired` is true for
|
|
259
|
+
* `invalid_grant`: Google's documented remedy is to "authenticate the user
|
|
260
|
+
* again", and no amount of retrying fixes it. Common causes: the user revoked
|
|
261
|
+
* access, the token went unused for six months, the OAuth app is still in
|
|
262
|
+
* "Testing" status (refresh tokens expire after 7 days), or the account passed
|
|
263
|
+
* 100 live refresh tokens for this client and the oldest was dropped.
|
|
264
|
+
*/
|
|
265
|
+
declare class GbpAuthError extends GbpError {
|
|
266
|
+
readonly code: GbpAuthErrorCode;
|
|
267
|
+
readonly reauthorizationRequired: boolean;
|
|
268
|
+
readonly status: number;
|
|
269
|
+
readonly description?: string;
|
|
270
|
+
constructor(code: GbpAuthErrorCode, status: number, description?: string);
|
|
271
|
+
}
|
|
272
|
+
/** A Business Profile API call returned an error status after its retry budget. */
|
|
273
|
+
declare class GbpApiError extends GbpError {
|
|
274
|
+
readonly operation: string;
|
|
275
|
+
readonly status: number;
|
|
276
|
+
readonly retryable: boolean;
|
|
277
|
+
readonly attempts: number;
|
|
278
|
+
/** Set when Google asked for a longer wait than this package will sleep through. */
|
|
279
|
+
readonly retryAfterMs?: number;
|
|
280
|
+
constructor(options: {
|
|
281
|
+
operation: string;
|
|
282
|
+
status: number;
|
|
283
|
+
retryable: boolean;
|
|
284
|
+
attempts: number;
|
|
285
|
+
body?: string;
|
|
286
|
+
retryAfterMs?: number;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** One attempt took longer than its deadline. Distinct from a caller's own abort. */
|
|
290
|
+
declare class GbpTimeoutError extends GbpError {
|
|
291
|
+
readonly operation: string;
|
|
292
|
+
readonly timeoutMs: number;
|
|
293
|
+
constructor(operation: string, timeoutMs: number);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pagination stopped for a reason that means something upstream is wrong:
|
|
297
|
+
* Google handed back a page token it had already given, or the page budget
|
|
298
|
+
* ran out. Thrown rather than returning a partial list, which would look like
|
|
299
|
+
* a complete one.
|
|
300
|
+
*/
|
|
301
|
+
declare class GbpPaginationError extends GbpError {
|
|
302
|
+
readonly reason: "repeated_token" | "page_limit";
|
|
303
|
+
readonly pagesFetched: number;
|
|
304
|
+
constructor(reason: "repeated_token" | "page_limit", pagesFetched: number);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { type AccessTokenOptions, type BusinessReview, type BusinessReviewsResult, DEFAULT_MAX_PAGES, DEFAULT_REQUEST_POLICY, GbpApiError, GbpAuthError, type GbpAuthErrorCode, GbpError, GbpPaginationError, type GbpRequestOptions, GbpTimeoutError, type GetBusinessReviewsOptions, type ReviewMedia, type ReviewOrder, type ReviewReply, type ReviewReplyState, getBusinessReviews, getGoogleOAuthAccessToken, hasGoogleOAuthCredentials, invalidateAccessToken, isBusinessProfileConfigured, parseListReviewsResponse, parseRetryAfter };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that decides how long to wait for Google and when to try again.
|
|
3
|
+
*
|
|
4
|
+
* Every request gets a deadline, because a stalled response would otherwise
|
|
5
|
+
* hold a Next.js render or build until the platform kills it. Rate limits
|
|
6
|
+
* (429) and transient server errors (500, 502, 503, 504) get a small, bounded
|
|
7
|
+
* number of retries with full-jitter backoff. Google documents the 429 for
|
|
8
|
+
* quota (developers.google.com/my-business/content/limits); whether the
|
|
9
|
+
* Business Profile API sends `Retry-After` is not documented, so it is honoured
|
|
10
|
+
* when present and not relied on.
|
|
11
|
+
*
|
|
12
|
+
* Nothing else is retried: a 400, 403 or 404 needs a person to change
|
|
13
|
+
* something, and repeating the request only delays the error that says so.
|
|
14
|
+
*/
|
|
15
|
+
interface GbpRequestOptions {
|
|
16
|
+
/** Deadline for each attempt, in ms. Default 8000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Attempts per request, counting the first. Default 3. */
|
|
19
|
+
maxAttempts?: number;
|
|
20
|
+
/** Base of the exponential backoff, in ms. Default 250. */
|
|
21
|
+
baseDelayMs?: number;
|
|
22
|
+
/** Cap on a single backoff, in ms. Default 5000. */
|
|
23
|
+
maxDelayMs?: number;
|
|
24
|
+
/** Longest `Retry-After` this will sleep through, in ms. Longer, and the error is returned instead. Default 30000. */
|
|
25
|
+
maxRetryAfterMs?: number;
|
|
26
|
+
/** Cancels everything, including a wait between attempts. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
declare const DEFAULT_REQUEST_POLICY: {
|
|
30
|
+
readonly timeoutMs: 8000;
|
|
31
|
+
readonly maxAttempts: 3;
|
|
32
|
+
readonly baseDelayMs: 250;
|
|
33
|
+
readonly maxDelayMs: 5000;
|
|
34
|
+
readonly maxRetryAfterMs: 30000;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* `Retry-After` as milliseconds: either delay-seconds or an HTTP-date (RFC
|
|
38
|
+
* 9110 section 10.2.3). `null` when absent or unreadable.
|
|
39
|
+
*/
|
|
40
|
+
declare function parseRetryAfter(value: string | null | undefined, nowMs?: number): number | null;
|
|
41
|
+
|
|
1
42
|
/**
|
|
2
43
|
* Google user OAuth (refresh-token) auth -- for APIs that reject service
|
|
3
44
|
* accounts, notably the Business Profile API (all scopes) and Search Console
|
|
@@ -9,17 +50,39 @@
|
|
|
9
50
|
* GBP_REFRESH_TOKEN values are set in the deploying app's environment -- not
|
|
10
51
|
* a code-level distinction. Each app/deployment configures its own.
|
|
11
52
|
*
|
|
53
|
+
* The access token is cached per process until a minute before it expires.
|
|
54
|
+
* Concurrent callers that miss the cache share one refresh rather than each
|
|
55
|
+
* starting their own. A caller that is told the token is no longer good (a
|
|
56
|
+
* 401 from the API) invalidates exactly that token and forces one refresh;
|
|
57
|
+
* see `invalidateAccessToken`.
|
|
58
|
+
*
|
|
12
59
|
* Env: GBP_CLIENT_ID, GBP_CLIENT_SECRET, GBP_REFRESH_TOKEN.
|
|
13
60
|
*/
|
|
61
|
+
|
|
14
62
|
/** True when the OAuth client + refresh token are present. */
|
|
15
63
|
declare function hasGoogleOAuthCredentials(): boolean;
|
|
64
|
+
interface AccessTokenOptions {
|
|
65
|
+
/** Ignore the cache and refresh now, e.g. after the API rejected the cached token. */
|
|
66
|
+
forceRefresh?: boolean;
|
|
67
|
+
/** Deadline and retry policy for the token request. */
|
|
68
|
+
request?: GbpRequestOptions;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Drop the cached token, but only if it is still the one the caller used. A
|
|
72
|
+
* request that was rejected with an old token must not throw away a newer one
|
|
73
|
+
* another request has already fetched.
|
|
74
|
+
*/
|
|
75
|
+
declare function invalidateAccessToken(accessToken: string): void;
|
|
16
76
|
/**
|
|
17
77
|
* Exchange the stored refresh token for a short-lived access token. Returns
|
|
18
78
|
* `null` when credentials aren't configured so callers can degrade
|
|
19
79
|
* gracefully. The token's actual scopes are whatever the refresh token was
|
|
20
80
|
* granted with -- not scoped per call.
|
|
81
|
+
*
|
|
82
|
+
* Throws `GbpAuthError`. On `invalid_grant` the cache is cleared and
|
|
83
|
+
* `reauthorizationRequired` is true: retrying cannot fix it.
|
|
21
84
|
*/
|
|
22
|
-
declare function getGoogleOAuthAccessToken(): Promise<string | null>;
|
|
85
|
+
declare function getGoogleOAuthAccessToken(options?: AccessTokenOptions): Promise<string | null>;
|
|
23
86
|
|
|
24
87
|
/**
|
|
25
88
|
* Google Business Profile -- fetch the business's own reviews, including the
|
|
@@ -43,10 +106,28 @@ declare function getGoogleOAuthAccessToken(): Promise<string | null>;
|
|
|
43
106
|
* Env: GBP_CLIENT_ID, GBP_CLIENT_SECRET, GBP_REFRESH_TOKEN,
|
|
44
107
|
* GOOGLE_BUSINESS_ACCOUNT_ID, GOOGLE_BUSINESS_LOCATION_ID.
|
|
45
108
|
*/
|
|
109
|
+
|
|
110
|
+
/** 50 pages of 50 is 2,500 reviews: far past any client's profile, well short of a runaway loop. */
|
|
111
|
+
declare const DEFAULT_MAX_PAGES = 50;
|
|
112
|
+
/** Google's moderation state for an owner reply. Replies from before moderation existed carry none. */
|
|
113
|
+
type ReviewReplyState = "PENDING" | "REJECTED" | "APPROVED";
|
|
46
114
|
interface ReviewReply {
|
|
47
115
|
text: string;
|
|
48
116
|
/** ISO 8601, when the reply was last posted/edited. */
|
|
49
117
|
updatedAt: string;
|
|
118
|
+
/** Moderation state, when Google reports one. */
|
|
119
|
+
state?: ReviewReplyState;
|
|
120
|
+
/** Why Google rejected the reply. Only set when `state` is `"REJECTED"`. New values may appear. */
|
|
121
|
+
policyViolation?: string;
|
|
122
|
+
}
|
|
123
|
+
/** A photo or video the reviewer attached. */
|
|
124
|
+
interface ReviewMedia {
|
|
125
|
+
/** The photo, or the video's thumbnail. */
|
|
126
|
+
thumbnailUrl: string;
|
|
127
|
+
/** The reviewer's own label for it, if any. */
|
|
128
|
+
label?: string;
|
|
129
|
+
/** Set for a video. */
|
|
130
|
+
videoUrl?: string;
|
|
50
131
|
}
|
|
51
132
|
interface BusinessReview {
|
|
52
133
|
id: string;
|
|
@@ -56,16 +137,61 @@ interface BusinessReview {
|
|
|
56
137
|
comment: string;
|
|
57
138
|
createdAt: string;
|
|
58
139
|
reply?: ReviewReply;
|
|
140
|
+
/** Photos and videos attached to the review, in Google's order. */
|
|
141
|
+
media?: ReviewMedia[];
|
|
142
|
+
/** Google's URL for replying to this review, for an owner-facing surface. */
|
|
143
|
+
replyUrl?: string;
|
|
59
144
|
}
|
|
60
145
|
interface BusinessReviewsResult {
|
|
146
|
+
/**
|
|
147
|
+
* Google's average for every review on the profile, rating-only ones
|
|
148
|
+
* included. `null` when Google did not report one (or the package is
|
|
149
|
+
* unconfigured): never computed from the returned list.
|
|
150
|
+
*/
|
|
61
151
|
averageRating: number | null;
|
|
62
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Google's count of every review on the profile. `null` when Google did not
|
|
154
|
+
* report one: never the length of the returned (filtered, limited) list,
|
|
155
|
+
* which would publish a wrong number into anything built from it.
|
|
156
|
+
*/
|
|
157
|
+
totalReviewCount: number | null;
|
|
63
158
|
reviews: BusinessReview[];
|
|
64
159
|
}
|
|
65
160
|
/** True when account + location + OAuth credentials are all configured. */
|
|
66
161
|
declare function isBusinessProfileConfigured(): boolean;
|
|
162
|
+
interface RawReview {
|
|
163
|
+
reviewId: string;
|
|
164
|
+
reviewer?: {
|
|
165
|
+
displayName?: string;
|
|
166
|
+
profilePhotoUrl?: string;
|
|
167
|
+
};
|
|
168
|
+
starRating?: string;
|
|
169
|
+
comment?: string;
|
|
170
|
+
createTime?: string;
|
|
171
|
+
reviewReply?: {
|
|
172
|
+
comment?: string;
|
|
173
|
+
updateTime?: string;
|
|
174
|
+
reviewReplyState?: string;
|
|
175
|
+
policyViolation?: string;
|
|
176
|
+
};
|
|
177
|
+
reviewMediaItems?: {
|
|
178
|
+
thumbnailUrl?: string;
|
|
179
|
+
thumbnailLabel?: string;
|
|
180
|
+
videoUrl?: string;
|
|
181
|
+
}[];
|
|
182
|
+
reviewReplyUrl?: string;
|
|
183
|
+
}
|
|
184
|
+
interface ReviewsPage {
|
|
185
|
+
reviews: RawReview[];
|
|
186
|
+
averageRating?: number;
|
|
187
|
+
totalReviewCount?: number;
|
|
188
|
+
nextPageToken?: string;
|
|
189
|
+
}
|
|
190
|
+
type ReviewOrder = "shuffle" | "api";
|
|
67
191
|
interface GetBusinessReviewsOptions {
|
|
68
|
-
/** Max reviews to return (default: all of them, paginated).
|
|
192
|
+
/** Max reviews to return (default: all of them, paginated). Counts usable
|
|
193
|
+
* reviews only -- see `filterMinStars` -- so this is "give me N reviews you
|
|
194
|
+
* can show", not "give me N raw API results". */
|
|
69
195
|
limit?: number;
|
|
70
196
|
/**
|
|
71
197
|
* When set, only reviews at or above this star rating are returned -- for
|
|
@@ -80,12 +206,102 @@ interface GetBusinessReviewsOptions {
|
|
|
80
206
|
revalidate?: number;
|
|
81
207
|
tags?: string[];
|
|
82
208
|
};
|
|
209
|
+
/**
|
|
210
|
+
* `"shuffle"` (default): randomizes the returned order, applied after
|
|
211
|
+
* `limit`. `"api"`: preserves the Business Profile API's own ordering
|
|
212
|
+
* (`updateTime desc`, most recent first).
|
|
213
|
+
*/
|
|
214
|
+
order?: ReviewOrder;
|
|
215
|
+
/**
|
|
216
|
+
* Keep owner replies Google has not approved (`PENDING` or `REJECTED`).
|
|
217
|
+
* Default `false`: a public page shows only replies Google shows, so a
|
|
218
|
+
* rejected reply never appears on the site as if it were live. Set `true`
|
|
219
|
+
* for an owner-facing surface, where `reply.state` and
|
|
220
|
+
* `reply.policyViolation` say what happened.
|
|
221
|
+
*/
|
|
222
|
+
includeUnapprovedReplies?: boolean;
|
|
223
|
+
/** Stop after this many pages rather than loop without end. Default 50. */
|
|
224
|
+
maxPages?: number;
|
|
225
|
+
/** Deadline and retry policy for every request this call makes. */
|
|
226
|
+
request?: GbpRequestOptions;
|
|
83
227
|
}
|
|
228
|
+
/** Validate a page of the reviews.list response. A malformed review is skipped; a malformed page throws. */
|
|
229
|
+
declare function parseListReviewsResponse(body: unknown): ReviewsPage;
|
|
84
230
|
/**
|
|
85
231
|
* Fetch reviews for the configured location, paginating through all pages
|
|
86
232
|
* (GBP caps each page at 50). Returns empty results (never throws on missing
|
|
87
233
|
* config) so UI can render unconditionally.
|
|
234
|
+
*
|
|
235
|
+
* Every request has a deadline and retries 429 and transient 5xx a bounded
|
|
236
|
+
* number of times. A 401 means the cached access token is no longer good: it
|
|
237
|
+
* is dropped, refreshed once, and the same page is asked for again. A second
|
|
238
|
+
* 401 throws. Throws `GbpApiError`, `GbpAuthError`, `GbpTimeoutError` or
|
|
239
|
+
* `GbpPaginationError`; catch and degrade in the calling route.
|
|
88
240
|
*/
|
|
89
241
|
declare function getBusinessReviews(options?: GetBusinessReviewsOptions): Promise<BusinessReviewsResult>;
|
|
90
242
|
|
|
91
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Typed failures, so a site can tell "reauthorise the Google account" from
|
|
245
|
+
* "Google is having a bad minute" from "our code looped", and log the
|
|
246
|
+
* difference without ever logging a credential.
|
|
247
|
+
*
|
|
248
|
+
* No message here carries a client id, client secret, refresh token, access
|
|
249
|
+
* token or review text. Bodies from Google are cut to a short, bounded
|
|
250
|
+
* excerpt.
|
|
251
|
+
*/
|
|
252
|
+
/** Base class: `instanceof GbpError` catches every failure this package throws. */
|
|
253
|
+
declare class GbpError extends Error {
|
|
254
|
+
constructor(message: string);
|
|
255
|
+
}
|
|
256
|
+
type GbpAuthErrorCode = "invalid_grant" | "invalid_client" | "token_request_failed";
|
|
257
|
+
/**
|
|
258
|
+
* The refresh-token exchange failed. `reauthorizationRequired` is true for
|
|
259
|
+
* `invalid_grant`: Google's documented remedy is to "authenticate the user
|
|
260
|
+
* again", and no amount of retrying fixes it. Common causes: the user revoked
|
|
261
|
+
* access, the token went unused for six months, the OAuth app is still in
|
|
262
|
+
* "Testing" status (refresh tokens expire after 7 days), or the account passed
|
|
263
|
+
* 100 live refresh tokens for this client and the oldest was dropped.
|
|
264
|
+
*/
|
|
265
|
+
declare class GbpAuthError extends GbpError {
|
|
266
|
+
readonly code: GbpAuthErrorCode;
|
|
267
|
+
readonly reauthorizationRequired: boolean;
|
|
268
|
+
readonly status: number;
|
|
269
|
+
readonly description?: string;
|
|
270
|
+
constructor(code: GbpAuthErrorCode, status: number, description?: string);
|
|
271
|
+
}
|
|
272
|
+
/** A Business Profile API call returned an error status after its retry budget. */
|
|
273
|
+
declare class GbpApiError extends GbpError {
|
|
274
|
+
readonly operation: string;
|
|
275
|
+
readonly status: number;
|
|
276
|
+
readonly retryable: boolean;
|
|
277
|
+
readonly attempts: number;
|
|
278
|
+
/** Set when Google asked for a longer wait than this package will sleep through. */
|
|
279
|
+
readonly retryAfterMs?: number;
|
|
280
|
+
constructor(options: {
|
|
281
|
+
operation: string;
|
|
282
|
+
status: number;
|
|
283
|
+
retryable: boolean;
|
|
284
|
+
attempts: number;
|
|
285
|
+
body?: string;
|
|
286
|
+
retryAfterMs?: number;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** One attempt took longer than its deadline. Distinct from a caller's own abort. */
|
|
290
|
+
declare class GbpTimeoutError extends GbpError {
|
|
291
|
+
readonly operation: string;
|
|
292
|
+
readonly timeoutMs: number;
|
|
293
|
+
constructor(operation: string, timeoutMs: number);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pagination stopped for a reason that means something upstream is wrong:
|
|
297
|
+
* Google handed back a page token it had already given, or the page budget
|
|
298
|
+
* ran out. Thrown rather than returning a partial list, which would look like
|
|
299
|
+
* a complete one.
|
|
300
|
+
*/
|
|
301
|
+
declare class GbpPaginationError extends GbpError {
|
|
302
|
+
readonly reason: "repeated_token" | "page_limit";
|
|
303
|
+
readonly pagesFetched: number;
|
|
304
|
+
constructor(reason: "repeated_token" | "page_limit", pagesFetched: number);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { type AccessTokenOptions, type BusinessReview, type BusinessReviewsResult, DEFAULT_MAX_PAGES, DEFAULT_REQUEST_POLICY, GbpApiError, GbpAuthError, type GbpAuthErrorCode, GbpError, GbpPaginationError, type GbpRequestOptions, GbpTimeoutError, type GetBusinessReviewsOptions, type ReviewMedia, type ReviewOrder, type ReviewReply, type ReviewReplyState, getBusinessReviews, getGoogleOAuthAccessToken, hasGoogleOAuthCredentials, invalidateAccessToken, isBusinessProfileConfigured, parseListReviewsResponse, parseRetryAfter };
|