@domandigital/gbp 0.3.0 → 0.3.2
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 +128 -0
- package/README.md +52 -13
- package/dist/index.cjs +9 -4
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +9 -4
- package/package.json +6 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@domandigital/gbp`.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
|
+
this project uses [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Releases before 0.3.1 were consumed as git dependencies pinned to a tag. This
|
|
9
|
+
file was written retroactively from that tag history, so entries below 0.3.1
|
|
10
|
+
describe what the diffs actually changed rather than what was recorded at the
|
|
11
|
+
time.
|
|
12
|
+
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [0.3.2] - 2026-08-16
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `GetBusinessReviewsOptions.order`, `"shuffle" | "api"`. Default is
|
|
20
|
+
`"shuffle"`, unchanged from prior behaviour. `"api"` preserves the Business
|
|
21
|
+
Profile API's own `updateTime desc` ordering instead.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **`limit` could silently under-return.** Pagination stopped once `raw.length`
|
|
26
|
+
reached `limit`, but the comment/star-rating filter that determines which
|
|
27
|
+
reviews are actually usable ran after the loop. A page that was mostly
|
|
28
|
+
star-only ratings with no comment -- which the API allows -- could satisfy
|
|
29
|
+
the raw count while producing far fewer, or zero, usable reviews, with no
|
|
30
|
+
error and no further pages fetched. The stopping condition now counts
|
|
31
|
+
usable reviews, so `limit` means "give me N reviews you can show."
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `limit`'s doc comment now says what it actually counts (usable reviews, not
|
|
36
|
+
raw API results), matching the fix above.
|
|
37
|
+
|
|
38
|
+
## [0.3.1] - 2026-08-16
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `./package.json` is now exposed in the `exports` map. Tooling that reads a
|
|
43
|
+
dependency's manifest previously hit `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
44
|
+
- `@types/node` is declared as a devDependency, and `tsconfig.json` sets
|
|
45
|
+
`"types": ["node"]`. This package uses `process.env`, `fetch`,
|
|
46
|
+
`URLSearchParams`, `RequestInit` and `Response`, all of which need those
|
|
47
|
+
types, and none of which had a declared source. `pnpm typecheck` passed only
|
|
48
|
+
because TypeScript walks up past the repo root and can find an `@types/node`
|
|
49
|
+
installed elsewhere on the machine. No consumer was affected, since published
|
|
50
|
+
`.d.ts` files carry their own references, but the check was not real.
|
|
51
|
+
- CI on every push and pull request: typecheck, test and build across Node 20,
|
|
52
|
+
22 and 24. Previously the only workflow was the tag-triggered release.
|
|
53
|
+
- README section covering `scripts/oauth-listener.mjs`, the one-time local flow
|
|
54
|
+
that mints a client's `GBP_REFRESH_TOKEN` and writes it straight to Doppler.
|
|
55
|
+
It shipped in 0.3.0 with no documentation at all.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- `engines.node` raised from `>=18` to `>=20`. Node 18 reached end of life in
|
|
60
|
+
April 2025, and the previous range was never exercised by anything.
|
|
61
|
+
- README rewritten for npm distribution. The install section still described the
|
|
62
|
+
package as unpublished and pointed at the `#v0.1.0` git tag.
|
|
63
|
+
|
|
64
|
+
## [0.3.0] - 2026-08-16
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- `scripts/oauth-listener.mjs`: a one-time local OAuth flow that prints a
|
|
69
|
+
consent URL, catches Google's redirect on a local listener, exchanges the
|
|
70
|
+
code, looks up the account and location IDs, and writes all three values to
|
|
71
|
+
Doppler. The token never passes through stdout, so it can't land in a
|
|
72
|
+
captured log.
|
|
73
|
+
|
|
74
|
+
This script is not part of the published package. It isn't in `files`, so it's
|
|
75
|
+
absent from the npm tarball: it shells out to the `doppler` binary, which has
|
|
76
|
+
no place in a public library's runtime contract. Clone the repo to use it.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- The pagination test asserted a fixed order against a result the library
|
|
81
|
+
deliberately shuffles. With two reviews a shuffle returns the input order
|
|
82
|
+
about half the time, so the test failed roughly 50% of runs and survived two
|
|
83
|
+
releases because nothing ran the suite automatically. It now compares the set
|
|
84
|
+
of ids.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- Licensed Apache-2.0, with publish metadata and `publishConfig` for public
|
|
89
|
+
access and provenance. First version published to the npm registry.
|
|
90
|
+
|
|
91
|
+
## [0.2.0] - 2026-07-31
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- **`getBusinessReviews` now returns reviews in random order.** Results are
|
|
96
|
+
shuffled before being returned, replacing the API's own
|
|
97
|
+
`orderBy=updateTime desc` ordering. There is no way to opt out.
|
|
98
|
+
|
|
99
|
+
Two consequences worth knowing before you upgrade from 0.1.0: output is not
|
|
100
|
+
stable across builds, so a statically generated testimonial section reorders
|
|
101
|
+
on every rebuild; and because the shuffle runs after `limit` is applied, you
|
|
102
|
+
get the most recent N reviews in random order rather than a random N.
|
|
103
|
+
|
|
104
|
+
(The 0.2.0 tag message also credits min-star filtering. That's inaccurate:
|
|
105
|
+
`filterMinStars` shipped in 0.1.0.)
|
|
106
|
+
|
|
107
|
+
## [0.1.0] - 2026-07-30
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
|
|
111
|
+
- Initial release, extracted from MMM Beauty's `packages/google-apis`. Zero
|
|
112
|
+
runtime dependencies.
|
|
113
|
+
- `getGoogleOAuthAccessToken` and `hasGoogleOAuthCredentials`: user
|
|
114
|
+
refresh-token OAuth, with the access token cached in process and refreshed a
|
|
115
|
+
minute before expiry. Service accounts are rejected by the Business Profile
|
|
116
|
+
API entirely, so this is the only auth route.
|
|
117
|
+
- `getBusinessReviews` and `isBusinessProfileConfigured`: a paginated reviews
|
|
118
|
+
fetch (the API caps each page at 50) that decodes Google's star-rating enums,
|
|
119
|
+
includes the business's reply to each review, and supports `limit`,
|
|
120
|
+
`filterMinStars` and a Next.js `next` cache hint. Returns empty results rather
|
|
121
|
+
than throwing when unconfigured, so UI can render unconditionally.
|
|
122
|
+
|
|
123
|
+
[Unreleased]: https://github.com/Doman-Digital/dd-gbp/compare/v0.3.2...HEAD
|
|
124
|
+
[0.3.2]: https://github.com/Doman-Digital/dd-gbp/compare/v0.3.1...v0.3.2
|
|
125
|
+
[0.3.1]: https://github.com/Doman-Digital/dd-gbp/compare/v0.3.0...v0.3.1
|
|
126
|
+
[0.3.0]: https://github.com/Doman-Digital/dd-gbp/compare/v0.2.0...v0.3.0
|
|
127
|
+
[0.2.0]: https://github.com/Doman-Digital/dd-gbp/compare/v0.1.0...v0.2.0
|
|
128
|
+
[0.1.0]: https://github.com/Doman-Digital/dd-gbp/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -53,19 +53,16 @@ with the reply attached, for free.
|
|
|
53
53
|
|
|
54
54
|
## Install
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@domandigital/gbp": "github:Doman-Digital/dd-gbp#v0.1.0"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
56
|
+
```bash
|
|
57
|
+
pnpm add @domandigital/gbp
|
|
64
58
|
```
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
Public on npm, Apache-2.0, published with provenance from a tagged release.
|
|
61
|
+
Zero runtime dependencies. ESM and CJS builds ship together, each with its own
|
|
62
|
+
types. Requires Node 20 or newer.
|
|
63
|
+
|
|
64
|
+
Upgrading a repo that still pins `github:Doman-Digital/dd-gbp#vX.Y.Z`? Swap it
|
|
65
|
+
for a semver range.
|
|
69
66
|
|
|
70
67
|
## Env
|
|
71
68
|
|
|
@@ -81,6 +78,42 @@ Which actual Google Cloud OAuth client + refresh token you point at (the
|
|
|
81
78
|
agency's shared credential, or a business's own dedicated one) is entirely a
|
|
82
79
|
deployment-env decision, not a code-level one -- each app configures its own.
|
|
83
80
|
|
|
81
|
+
## Minting a client's refresh token
|
|
82
|
+
|
|
83
|
+
`GBP_REFRESH_TOKEN` can only be obtained by a human completing Google's consent
|
|
84
|
+
flow as the business owner. `scripts/oauth-listener.mjs` in this repo automates
|
|
85
|
+
everything around that: it prints a consent URL, catches Google's redirect on a
|
|
86
|
+
local listener, exchanges the code, looks up the account and location IDs, and
|
|
87
|
+
writes all three values straight to Doppler.
|
|
88
|
+
|
|
89
|
+
The token and both IDs travel via argv into `doppler secrets set` and are never
|
|
90
|
+
printed to stdout or returned from the process, so a captured terminal log can't
|
|
91
|
+
leak them.
|
|
92
|
+
|
|
93
|
+
This script is repo-only. It isn't in the package's `files`, so it's absent from
|
|
94
|
+
the npm tarball, deliberately: it shells out to the `doppler` binary, which has
|
|
95
|
+
no business being a runtime expectation of a public library. Clone the repo to
|
|
96
|
+
use it.
|
|
97
|
+
|
|
98
|
+
Run it from inside the **client's** repo, so `doppler secrets set` targets that
|
|
99
|
+
client's own project and config via their local `.doppler.yaml` scoping:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
GBP_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
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Two things that will bite you once each:
|
|
106
|
+
|
|
107
|
+
- `http://127.0.0.1:3333/oauth2callback` has to be registered as an authorised
|
|
108
|
+
redirect URI on the OAuth client in the GCP console. If it isn't, Google
|
|
109
|
+
returns `redirect_uri_mismatch`.
|
|
110
|
+
- If the account has already granted this app access, Google can decline to
|
|
111
|
+
issue a new refresh token even with `prompt=consent`. Revoke it at
|
|
112
|
+
https://myaccount.google.com/permissions and run the script again.
|
|
113
|
+
|
|
114
|
+
If the account has more than one location, the script uses the first and prints
|
|
115
|
+
every location it saw, so you can check it picked the right one.
|
|
116
|
+
|
|
84
117
|
## API
|
|
85
118
|
|
|
86
119
|
### `getBusinessReviews(options?): Promise<BusinessReviewsResult>`
|
|
@@ -107,8 +140,9 @@ interface BusinessReview {
|
|
|
107
140
|
|
|
108
141
|
Options:
|
|
109
142
|
|
|
110
|
-
- `limit?: number` -- stop paginating once this many reviews are
|
|
111
|
-
|
|
143
|
+
- `limit?: number` -- stop paginating once this many *usable* reviews are
|
|
144
|
+
collected (a raw result needs both a comment and a star rating to count, and
|
|
145
|
+
`filterMinStars` narrows it further). Omit to fetch all of them.
|
|
112
146
|
- `filterMinStars?: number` -- drop reviews below this rating. Use for a
|
|
113
147
|
public testimonial feed; omit for an owner-facing surface where the point
|
|
114
148
|
is to see everything. `totalReviewCount` always reflects the location's
|
|
@@ -116,6 +150,11 @@ Options:
|
|
|
116
150
|
- `next?: { revalidate?: number; tags?: string[] }` -- passed straight through
|
|
117
151
|
to `fetch`'s Next.js cache-hint augmentation. A no-op outside Next.js.
|
|
118
152
|
Default: revalidate hourly.
|
|
153
|
+
- `order?: "shuffle" | "api"` -- `"shuffle"` (default) randomizes the returned
|
|
154
|
+
order, applied after `limit`. `"api"` preserves the Business Profile API's
|
|
155
|
+
own ordering (`updateTime desc`, most recent first). Pick `"api"` for
|
|
156
|
+
anything that should read as chronological, e.g. an activity feed; the
|
|
157
|
+
default suits a testimonial grid where a fixed order would look stale.
|
|
119
158
|
|
|
120
159
|
Never throws on missing configuration -- `isBusinessProfileConfigured()` gates
|
|
121
160
|
internally and returns an empty result, so UI can render unconditionally.
|
package/dist/index.cjs
CHANGED
|
@@ -79,6 +79,11 @@ function shuffleArray(arr) {
|
|
|
79
79
|
}
|
|
80
80
|
return copy;
|
|
81
81
|
}
|
|
82
|
+
function isUsableReview(r, filterMinStars) {
|
|
83
|
+
if (!r.comment || !r.starRating) return false;
|
|
84
|
+
if (filterMinStars !== void 0 && STAR_VALUES[r.starRating] < filterMinStars) return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
82
87
|
var EMPTY = {
|
|
83
88
|
averageRating: null,
|
|
84
89
|
totalReviewCount: 0,
|
|
@@ -102,7 +107,7 @@ function toBusinessReview(r) {
|
|
|
102
107
|
return review;
|
|
103
108
|
}
|
|
104
109
|
async function getBusinessReviews(options = {}) {
|
|
105
|
-
const { limit, filterMinStars, next } = options;
|
|
110
|
+
const { limit, filterMinStars, next, order = "shuffle" } = options;
|
|
106
111
|
if (!isBusinessProfileConfigured()) return EMPTY;
|
|
107
112
|
const token = await getGoogleOAuthAccessToken();
|
|
108
113
|
if (!token) return EMPTY;
|
|
@@ -127,12 +132,12 @@ async function getBusinessReviews(options = {}) {
|
|
|
127
132
|
averageRating = data.averageRating ?? averageRating;
|
|
128
133
|
totalReviewCount = data.totalReviewCount ?? totalReviewCount;
|
|
129
134
|
pageToken = data.nextPageToken;
|
|
130
|
-
} while (pageToken && (limit === void 0 || raw.length < limit));
|
|
131
|
-
const reviews = raw.filter((r) =>
|
|
135
|
+
} while (pageToken && (limit === void 0 || raw.filter((r) => isUsableReview(r, filterMinStars)).length < limit));
|
|
136
|
+
const reviews = raw.filter((r) => isUsableReview(r, filterMinStars)).map(toBusinessReview).slice(0, limit);
|
|
132
137
|
return {
|
|
133
138
|
averageRating: averageRating ?? null,
|
|
134
139
|
totalReviewCount: totalReviewCount ?? reviews.length,
|
|
135
|
-
reviews: shuffleArray(reviews)
|
|
140
|
+
reviews: order === "api" ? reviews : shuffleArray(reviews)
|
|
136
141
|
};
|
|
137
142
|
}
|
|
138
143
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.d.cts
CHANGED
|
@@ -64,8 +64,11 @@ interface BusinessReviewsResult {
|
|
|
64
64
|
}
|
|
65
65
|
/** True when account + location + OAuth credentials are all configured. */
|
|
66
66
|
declare function isBusinessProfileConfigured(): boolean;
|
|
67
|
+
type ReviewOrder = "shuffle" | "api";
|
|
67
68
|
interface GetBusinessReviewsOptions {
|
|
68
|
-
/** Max reviews to return (default: all of them, paginated).
|
|
69
|
+
/** Max reviews to return (default: all of them, paginated). Counts usable
|
|
70
|
+
* reviews only -- see `filterMinStars` -- so this is "give me N reviews you
|
|
71
|
+
* can show", not "give me N raw API results". */
|
|
69
72
|
limit?: number;
|
|
70
73
|
/**
|
|
71
74
|
* When set, only reviews at or above this star rating are returned -- for
|
|
@@ -80,6 +83,12 @@ interface GetBusinessReviewsOptions {
|
|
|
80
83
|
revalidate?: number;
|
|
81
84
|
tags?: string[];
|
|
82
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* `"shuffle"` (default): randomizes the returned order, applied after
|
|
88
|
+
* `limit`. `"api"`: preserves the Business Profile API's own ordering
|
|
89
|
+
* (`updateTime desc`, most recent first).
|
|
90
|
+
*/
|
|
91
|
+
order?: ReviewOrder;
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* Fetch reviews for the configured location, paginating through all pages
|
package/dist/index.d.ts
CHANGED
|
@@ -64,8 +64,11 @@ interface BusinessReviewsResult {
|
|
|
64
64
|
}
|
|
65
65
|
/** True when account + location + OAuth credentials are all configured. */
|
|
66
66
|
declare function isBusinessProfileConfigured(): boolean;
|
|
67
|
+
type ReviewOrder = "shuffle" | "api";
|
|
67
68
|
interface GetBusinessReviewsOptions {
|
|
68
|
-
/** Max reviews to return (default: all of them, paginated).
|
|
69
|
+
/** Max reviews to return (default: all of them, paginated). Counts usable
|
|
70
|
+
* reviews only -- see `filterMinStars` -- so this is "give me N reviews you
|
|
71
|
+
* can show", not "give me N raw API results". */
|
|
69
72
|
limit?: number;
|
|
70
73
|
/**
|
|
71
74
|
* When set, only reviews at or above this star rating are returned -- for
|
|
@@ -80,6 +83,12 @@ interface GetBusinessReviewsOptions {
|
|
|
80
83
|
revalidate?: number;
|
|
81
84
|
tags?: string[];
|
|
82
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* `"shuffle"` (default): randomizes the returned order, applied after
|
|
88
|
+
* `limit`. `"api"`: preserves the Business Profile API's own ordering
|
|
89
|
+
* (`updateTime desc`, most recent first).
|
|
90
|
+
*/
|
|
91
|
+
order?: ReviewOrder;
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* Fetch reviews for the configured location, paginating through all pages
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,11 @@ function shuffleArray(arr) {
|
|
|
50
50
|
}
|
|
51
51
|
return copy;
|
|
52
52
|
}
|
|
53
|
+
function isUsableReview(r, filterMinStars) {
|
|
54
|
+
if (!r.comment || !r.starRating) return false;
|
|
55
|
+
if (filterMinStars !== void 0 && STAR_VALUES[r.starRating] < filterMinStars) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
53
58
|
var EMPTY = {
|
|
54
59
|
averageRating: null,
|
|
55
60
|
totalReviewCount: 0,
|
|
@@ -73,7 +78,7 @@ function toBusinessReview(r) {
|
|
|
73
78
|
return review;
|
|
74
79
|
}
|
|
75
80
|
async function getBusinessReviews(options = {}) {
|
|
76
|
-
const { limit, filterMinStars, next } = options;
|
|
81
|
+
const { limit, filterMinStars, next, order = "shuffle" } = options;
|
|
77
82
|
if (!isBusinessProfileConfigured()) return EMPTY;
|
|
78
83
|
const token = await getGoogleOAuthAccessToken();
|
|
79
84
|
if (!token) return EMPTY;
|
|
@@ -98,12 +103,12 @@ async function getBusinessReviews(options = {}) {
|
|
|
98
103
|
averageRating = data.averageRating ?? averageRating;
|
|
99
104
|
totalReviewCount = data.totalReviewCount ?? totalReviewCount;
|
|
100
105
|
pageToken = data.nextPageToken;
|
|
101
|
-
} while (pageToken && (limit === void 0 || raw.length < limit));
|
|
102
|
-
const reviews = raw.filter((r) =>
|
|
106
|
+
} while (pageToken && (limit === void 0 || raw.filter((r) => isUsableReview(r, filterMinStars)).length < limit));
|
|
107
|
+
const reviews = raw.filter((r) => isUsableReview(r, filterMinStars)).map(toBusinessReview).slice(0, limit);
|
|
103
108
|
return {
|
|
104
109
|
averageRating: averageRating ?? null,
|
|
105
110
|
totalReviewCount: totalReviewCount ?? reviews.length,
|
|
106
|
-
reviews: shuffleArray(reviews)
|
|
111
|
+
reviews: order === "api" ? reviews : shuffleArray(reviews)
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
114
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domandigital/gbp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"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.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
"types": "./dist/index.d.cts",
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
23
|
+
"CHANGELOG.md",
|
|
22
24
|
"LICENSE",
|
|
23
25
|
"README.md",
|
|
24
26
|
"dist"
|
|
@@ -30,12 +32,13 @@
|
|
|
30
32
|
"prepublishOnly": "pnpm run build"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
35
|
+
"@types/node": "^20",
|
|
33
36
|
"tsup": "^8.3.5",
|
|
34
37
|
"typescript": "^5.6.3",
|
|
35
38
|
"vitest": "^2.1.9"
|
|
36
39
|
},
|
|
37
40
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
41
|
+
"node": ">=20"
|
|
39
42
|
},
|
|
40
43
|
"packageManager": "pnpm@9.15.9",
|
|
41
44
|
"repository": {
|