{"_id":"@domandigital/gbp","_rev":"4-ff28e7740308f04ef1b3bbbacdd38477","name":"@domandigital/gbp","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.0":{"name":"@domandigital/gbp","version":"0.3.0","license":"Apache-2.0","_id":"@domandigital/gbp@0.3.0","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-gbp#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-gbp/issues"},"dist":{"shasum":"af7a0156ed692aff708ea82318b6ce16804c2291","tarball":"https://registry.npmjs.org/@domandigital/gbp/-/gbp-0.3.0.tgz","fileCount":7,"integrity":"sha512-JpBXbpadlndy6gtavMmJMHayUdIl1cNuwJqcr2t9KBRcHTxGVNg0QdT/UTlLaK9YLxhEy/lpB8xzACEPU1BIsg==","signatures":[{"sig":"MEQCIEtYsvtVrFrmneutbd8G99jNowa/XFaa3C/HGOMzUFZkAiBWiT080N0Sy5h4xvyA9XY+c69jwKc8qdKozlFDX85Mig==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":35320},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"5716d0210af38f7430de8d1fb4596a5b93a4fb79","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"domandigital","email":"admin@domandigital.co.uk"},"repository":{"url":"git+https://github.com/Doman-Digital/dd-gbp.git","type":"git"},"_npmVersion":"11.8.0","description":"Zero-dependency Google Business Profile client: OAuth refresh-token auth and a reviews (with owner replies) fetch, shared across Doman Digital / IDS client sites.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/gbp_0.3.0_1786897273869_0.21385300996992584","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@domandigital/gbp","version":"0.3.1","license":"Apache-2.0","_id":"@domandigital/gbp@0.3.1","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-gbp#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-gbp/issues"},"dist":{"shasum":"5f4f82b3ea540347d639697aebbb48f64473a459","tarball":"https://registry.npmjs.org/@domandigital/gbp/-/gbp-0.3.1.tgz","fileCount":8,"integrity":"sha512-SeV+qmFGvi9mo3V9Cu7GJqH0dtqqSGphSsJitwpUuxswzowbg5cnCkwgtSvP+E/misLdOuW5Gcw+Kb3LSr/p1w==","signatures":[{"sig":"MEQCIADFMjKtJypQaLKIw71VxrlavcwERsTxv3dnd/iJeXijAiBSTjIobeZpXsDcabYwUFE1oKOJnnoOnBeYQ5lkoD7LbQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fgbp@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":41893},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"b39d678023ac5ef241db34f147a5e2e524e0d49c","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f246dc18-8abe-4c98-91d4-32b8b03d0597"}},"repository":{"url":"git+https://github.com/Doman-Digital/dd-gbp.git","type":"git"},"_npmVersion":"12.0.2","description":"Zero-dependency Google Business Profile client: OAuth refresh-token auth and a reviews (with owner replies) fetch, shared across Doman Digital / IDS client sites.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3","@types/node":"^20"},"_npmOperationalInternal":{"tmp":"tmp/gbp_0.3.1_1786908866370_0.0572239365310534","host":"s3://npm-registry-packages-npm-production"}},"0.3.2":{"name":"@domandigital/gbp","version":"0.3.2","license":"Apache-2.0","_id":"@domandigital/gbp@0.3.2","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-gbp#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-gbp/issues"},"dist":{"shasum":"43bbc21cef304ce99fc40e6ab0d6867fba084faf","tarball":"https://registry.npmjs.org/@domandigital/gbp/-/gbp-0.3.2.tgz","fileCount":8,"integrity":"sha512-U7SFV2I9lnyfdUK+mJyZvaYtK9mK0ZSOtgSsf9YwpImOOjPH0FRrXg/CZbgT4ViqXWW88sQvU4BS0YmKFGwRsg==","signatures":[{"sig":"MEUCIEyEuNoF55OpuBX4PJya17OZD7gujiuHzqjreUFanSlYAiEAoqxsiVL/I2qQBdQdsV8xDoLjcvSBnh4ufwPJYW5G9zg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fgbp@0.3.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":44650},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"ee72c8a0eec5b2e360f290a54f723d509426054d","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f246dc18-8abe-4c98-91d4-32b8b03d0597"}},"repository":{"url":"git+https://github.com/Doman-Digital/dd-gbp.git","type":"git"},"_npmVersion":"12.0.2","description":"Zero-dependency Google Business Profile client: OAuth refresh-token auth and a reviews (with owner replies) fetch, shared across Doman Digital / IDS client sites.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3","@types/node":"^20"},"_npmOperationalInternal":{"tmp":"tmp/gbp_0.3.2_1786911369651_0.6216799518952598","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"@domandigital/gbp@0.4.0","bugs":{"url":"https://github.com/Doman-Digital/dd-packages/issues"},"dist":{"shasum":"412a2fcbfa915e5a7a792d52cf1c9a3aeff67fe6","tarball":"https://registry.npmjs.org/@domandigital/gbp/-/gbp-0.4.0.tgz","fileCount":8,"integrity":"sha512-lkGx1pCLN5XMEHhHrRo3l+N3x7CQX1k8+crYFsUn3Dj21mMFBnJ/KA78fncumnnKGM16hUqHA8dH/g+Ab5adHg==","signatures":[{"sig":"MEQCIE8YZ/lWeEm/PCGz5pahwzeUBVL3tLWPJkP4+YnVc2p6AiAsW8PRkObyoKpedCF3c480z+y0qKdtU1OC0qwSYJNh9g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCH7g+82iIpsSKqtjP5Vb+Bt/CzGaCpEb2Dx9ViuE2KWQIhAOrikfKxooXtLBIXc4LEchh1YzKD7v9S5SX+fKEO0EYZ"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fgbp@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":91253},"main":"dist/index.cjs","name":"@domandigital/gbp","type":"module","_from":"file:domandigital-gbp-0.4.0.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.12"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"license":"Apache-2.0","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"version":"0.4.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3449eea9-f24e-404d-8791-49851256e022"}},"homepage":"https://github.com/Doman-Digital/dd-packages/tree/main/packages/gbp#readme","_resolved":"/tmp/e86e2d202dee3e5bcd694d1de9e04689/domandigital-gbp-0.4.0.tgz","_integrity":"sha512-lkGx1pCLN5XMEHhHrRo3l+N3x7CQX1k8+crYFsUn3Dj21mMFBnJ/KA78fncumnnKGM16hUqHA8dH/g+Ab5adHg==","repository":{"url":"git+https://github.com/Doman-Digital/dd-packages.git","type":"git","directory":"packages/gbp"},"_npmVersion":"11.20.0","description":"Zero-dependency Google Business Profile client: OAuth refresh-token auth and a reviews (with owner replies) fetch, shared across Doman Digital / IDS client sites.","directories":{},"maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3","@types/node":"^20"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gbp_0.4.0_1790215100346_0.15693416194393173"}}},"time":{"created":"2026-08-16T16:21:13.643Z","modified":"2026-09-24T01:58:20.807Z","0.3.0":"2026-08-16T16:21:14.007Z","0.3.1":"2026-08-16T19:34:26.530Z","0.3.2":"2026-08-16T20:16:09.817Z","0.4.0":"2026-09-24T01:58:20.444Z"},"bugs":{"url":"https://github.com/Doman-Digital/dd-packages/issues"},"license":"Apache-2.0","homepage":"https://github.com/Doman-Digital/dd-packages/tree/main/packages/gbp#readme","repository":{"url":"git+https://github.com/Doman-Digital/dd-packages.git","type":"git","directory":"packages/gbp"},"description":"Zero-dependency Google Business Profile client: OAuth refresh-token auth and a reviews (with owner replies) fetch, shared across Doman Digital / IDS client sites.","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"readme":"# @domandigital/gbp\n\nA zero-dependency Google Business Profile client for Doman Digital / IDS\nclient sites: OAuth refresh-token auth and a paginated reviews fetch,\nincluding the business's own reply to each review.\n\n## Why this exists\n\nExtracted from MMM Beauty's `packages/google-apis` (`business-profile.ts` +\n`oauth-client.ts`), which was already proven in production against the\nreal GBP API's quirks -- star-rating enums, page-size caps, graceful\ndegrade-to-empty when unconfigured. Six client sites need this, not one, so\nit moved out into its own package rather than being copy-pasted five more\ntimes. That's the exact drift `@domandigital/graph` already exists to\nprevent, one layer down: OAuth + pagination + enum-decoding logic\nindependently reinvented per repo instead of fixed once.\n\nThis package does **not** depend on `@domandigital/graph` and vice versa.\n`gbp` fetches review facts; `graph` turns facts into JSON-LD. A consuming\nsite composes them at the call site:\n\n```ts\nimport { getBusinessReviews } from \"@domandigital/gbp\";\nimport { buildReview, createGraphIds } from \"@domandigital/graph\";\n\nconst { reviews, averageRating, totalReviewCount } = await getBusinessReviews();\nconst reviewNodes = reviews.map((r) =>\n  buildReview(\n    {\n      authorName: r.author,\n      reviewBody: r.comment,\n      ratingValue: r.rating,\n      datePublished: r.createdAt,\n      reply: r.reply ? { text: r.reply.text, dateCreated: r.reply.updatedAt } : null,\n    },\n    ids,\n  ),\n);\n```\n\n`averageRating` / `totalReviewCount` are Google's own numbers for the whole\nprofile, rating-only reviews included. Show them instead of hardcoding a\nrating/count literal in a site's settings file: that literal drifting from the\nreal listing (a site claiming 7 reviews when Google has 6) is the bug this\npackage exists to kill. Both are `null` when Google did not report them;\nrender nothing in that case rather than a number.\n\n### Structured data: these reviews earn no stars on the business's own site\n\nGoogle calls a review \"self-serving\" when a review about a business sits on\nthat business's own website, whether written into the markup or through an\nembedded widget. For `LocalBusiness` and `Organization`, Google shows review\nstars only \"for sites that capture reviews about other\" businesses\n(developers.google.com/search/docs/appearance/structured-data/review-snippet,\nupdated 2026-09-08). So `Review` or `AggregateRating` markup built from these\nreviews, on the client's own site, is ineligible for stars.\n\nIt is not a penalty: Google's announcement of the rule says you do not need to\nremove such markup and \"You won't get a manual action just for this\"\n(developers.google.com/search/blog/2019/09/making-review-rich-results-more-helpful).\nShowing the reviews on the page is unaffected. Just do not sell or expect the\nstars. `@domandigital/graph`'s `findGraphIssues` can flag the pattern.\n\n## Why Business Profile API, not Places API\n\nPlaces API's `Review` object has no field for the business's reply, full\nstop -- and caps out at 5 reviews per place with no pagination. Neither\nlimitation applies here: this uses the Business Profile API (the same one\nyou already manage the listing through), which returns every review, paginated,\nwith the reply attached, for free.\n\n## Install\n\n```bash\npnpm add @domandigital/gbp\n```\n\nPublic on npm, Apache-2.0, published with provenance from a tagged release.\nZero runtime dependencies. ESM and CJS builds ship together, each with its own\ntypes. Requires Node 22.12 or newer.\n\nUpgrading a repo that still pins `github:Doman-Digital/dd-gbp#vX.Y.Z`? Swap it\nfor a semver range.\n\n## Env\n\n| Var | Purpose |\n|---|---|\n| `GBP_CLIENT_ID` | Google Cloud OAuth client id |\n| `GBP_CLIENT_SECRET` | Google Cloud OAuth client secret |\n| `GBP_REFRESH_TOKEN` | User refresh token (service accounts are rejected by this API) |\n| `GOOGLE_BUSINESS_ACCOUNT_ID` | e.g. `accounts/1234567890` |\n| `GOOGLE_BUSINESS_LOCATION_ID` | e.g. `locations/9876543210` |\n\nWhich actual Google Cloud OAuth client + refresh token you point at (the\nagency's shared credential, or a business's own dedicated one) is entirely a\ndeployment-env decision, not a code-level one -- each app configures its own.\n\n## Minting a client's refresh token\n\n`GBP_REFRESH_TOKEN` can only be obtained by a human completing Google's consent\nflow as the business owner. `scripts/oauth-listener.mjs` in this repo automates\neverything around that: it prints a consent URL, catches Google's redirect on a\nlocal listener, exchanges the code, looks up the account and location IDs, and\nwrites all three values straight to Doppler.\n\nThe token and both IDs travel via argv into `doppler secrets set` and are never\nprinted to stdout or returned from the process, so a captured terminal log can't\nleak them.\n\nThis script is repo-only. It isn't in the package's `files`, so it's absent from\nthe npm tarball, deliberately: it shells out to the `doppler` binary, which has\nno business being a runtime expectation of a public library. Clone the repo to\nuse it.\n\nRun it from inside the **client's** repo, so `doppler secrets set` targets that\nclient's own project and config via their local `.doppler.yaml` scoping:\n\n```bash\nGBP_CLIENT_ID=$(doppler secrets get GBP_CLIENT_ID --plain) GBP_CLIENT_SECRET=$(doppler secrets get GBP_CLIENT_SECRET --plain) node ../dd-gbp/scripts/oauth-listener.mjs\n```\n\nTwo things that will bite you once each:\n\n- `http://127.0.0.1:3333/oauth2callback` has to be registered as an authorised\n  redirect URI on the OAuth client in the GCP console. If it isn't, Google\n  returns `redirect_uri_mismatch`.\n- If the account has already granted this app access, Google can decline to\n  issue a new refresh token even with `prompt=consent`. Revoke it at\n  https://myaccount.google.com/permissions and run the script again.\n\nIf the account has more than one location, the script uses the first and prints\nevery location it saw, so you can check it picked the right one.\n\n## API\n\n### `getBusinessReviews(options?): Promise<BusinessReviewsResult>`\n\nPaginates through every review (GBP caps each page at 50) and returns:\n\n```ts\ninterface BusinessReviewsResult {\n  averageRating: number | null;    // Google's, for the whole profile; null if not reported\n  totalReviewCount: number | null; // Google's, for the whole profile; null if not reported\n  reviews: BusinessReview[];\n}\n\ninterface BusinessReview {\n  id: string;\n  author: string;\n  authorPhoto?: string;\n  rating: number; // 1-5\n  comment: string;\n  createdAt: string; // ISO\n  reply?: {        // no author field -- see below\n    text: string;\n    updatedAt: string;\n    state?: \"PENDING\" | \"REJECTED\" | \"APPROVED\"; // Google's moderation state\n    policyViolation?: string;                    // why, when REJECTED\n  };\n  media?: { thumbnailUrl: string; label?: string; videoUrl?: string }[]; // photos/videos the reviewer attached\n  replyUrl?: string; // Google's URL for replying, for an owner-facing surface\n}\n```\n\n`totalReviewCount` and `averageRating` are never computed from the returned\nlist. Before 0.4.0, a missing `totalReviewCount` fell back to the length of\nthe filtered, limited list, which would have published a wrong count.\n\nOptions:\n\n- `limit?: number` -- stop paginating once this many *usable* reviews are\n  collected (a raw result needs both a comment and a star rating to count, and\n  `filterMinStars` narrows it further). Omit to fetch all of them.\n- `filterMinStars?: number` -- drop reviews below this rating. Use for a\n  public testimonial feed; omit for an owner-facing surface where the point\n  is to see everything. `totalReviewCount` always reflects the location's\n  real total, never the filtered count.\n- `next?: { revalidate?: number; tags?: string[] }` -- passed straight through\n  to `fetch`'s Next.js cache-hint augmentation. A no-op outside Next.js.\n  Default: revalidate hourly.\n- `order?: \"shuffle\" | \"api\"` -- `\"shuffle\"` (default) randomizes the returned\n  order, applied after `limit`. `\"api\"` preserves the Business Profile API's\n  own ordering (`updateTime desc`, most recent first). Pick `\"api\"` for\n  anything that should read as chronological, e.g. an activity feed; the\n  default suits a testimonial grid where a fixed order would look stale.\n- `includeUnapprovedReplies?: boolean` -- default `false`: an owner reply\n  Google has marked `PENDING` or `REJECTED` is left off, so a public page never\n  shows a reply Google does not. Set `true` for an owner-facing surface, where\n  `reply.state` and `reply.policyViolation` say what happened.\n- `maxPages?: number` -- stop after this many pages (default 50, i.e. 2,500\n  reviews) rather than loop.\n- `request?: { timeoutMs?, maxAttempts?, baseDelayMs?, maxDelayMs?, maxRetryAfterMs?, signal? }`\n  -- deadline and retry policy for every request the call makes. Defaults: 8 s\n  per attempt, 3 attempts, full-jitter backoff from 250 ms capped at 5 s, and a\n  `Retry-After` of up to 30 s honoured.\n\nNever throws on missing configuration -- `isBusinessProfileConfigured()` gates\ninternally and returns an empty result, so UI can render unconditionally.\nDoes throw on a real failure, so a calling route should catch and degrade\nexplicitly if it wants zero-downtime behaviour on a Google outage.\n\n### Failures, and what each one means\n\nEvery request has a deadline. A 429 or a transient 500/502/503/504 is retried\na few times with backoff; Google documents the 429 for quota\n(developers.google.com/my-business/content/limits). Nothing else is retried.\nA 401 means the cached access token is no longer good: it is dropped,\nrefreshed once, and the page is asked for again; a second 401 throws.\n\nAll errors extend `GbpError`, and none carries a credential:\n\n| Error | When | What to do |\n|---|---|---|\n| `GbpAuthError` with `reauthorizationRequired: true` (`code: \"invalid_grant\"`) | Google refused the refresh token | Reauthorise the account and replace `GBP_REFRESH_TOKEN`. Retrying does nothing. |\n| `GbpAuthError` (`code: \"invalid_client\"`) | The client id or secret is wrong | Fix the environment. |\n| `GbpApiError` | The API returned an error after the retry budget. `status`, `retryable`, `attempts`, and `retryAfterMs` when Google asked for a long wait | Degrade, and try again on the next revalidation. |\n| `GbpTimeoutError` | An attempt passed its deadline | As above. |\n| `GbpPaginationError` | Google repeated a page token, or `maxPages` ran out | Report it: something upstream is wrong. |\n\nRefresh tokens stop working when the user revokes access, when a token goes\nunused for six months, when the account passes 100 live refresh tokens for\nthe client (the oldest is dropped without warning), and **after 7 days if the\nOAuth app is still in \"Testing\" status**: publish the app, or expect weekly\nreauthorisation (developers.google.com/identity/protocols/oauth2). Google can\nalso delete an OAuth client that goes unused, recoverable for 30 days\n(developers.google.com/identity/protocols/oauth2/web-server).\n\n### Why a reply has no author field\n\nA GBP review has exactly one reply slot, and it can only ever come from the\nbusiness -- there is no third party to disambiguate, so the API returns\n`{ comment, updateTime, reviewReplyState }` with nothing else. Google's own\nlisting UI renders every reply as \"Response from the owner\" for the same\nreason. Attribute it to the business's own Organization node in the\nconsumer (see the `@domandigital/graph` composition example above) --\ndon't go looking for an author field that doesn't exist.\n\n### `isBusinessProfileConfigured(): boolean`\n\nTrue once all five env vars above are set.\n\n### `hasGoogleOAuthCredentials()` / `getGoogleOAuthAccessToken()`\n\nLower-level OAuth primitives, exported in case a consumer needs the raw\naccess token for another Business Profile endpoint this package doesn't wrap\n(e.g. business hours). The token is cached in-process and refreshed a minute\nbefore expiry; concurrent callers share one refresh.\n`getGoogleOAuthAccessToken({ forceRefresh: true })` refreshes now, and\n`invalidateAccessToken(token)` drops a token an API has rejected (only if it is\nstill the cached one).\n","readmeFilename":"README.md"}