@vendure-platform/console-auth 1.0.0-major.202609091053.0.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/README.md +335 -0
- package/dist/browser-auth.d.ts +48 -0
- package/dist/browser-auth.js +1 -0
- package/dist/console-api.d.ts +66 -0
- package/dist/console-api.js +1 -0
- package/dist/console-projects.d.ts +101 -0
- package/dist/console-projects.js +1 -0
- package/dist/credential-store.d.ts +99 -0
- package/dist/credential-store.js +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -0
- package/dist/onboarding.d.ts +158 -0
- package/dist/onboarding.js +1 -0
- package/dist/origins.d.ts +23 -0
- package/dist/origins.js +1 -0
- package/dist/product-access.d.ts +48 -0
- package/dist/product-access.js +1 -0
- package/dist/project-link-manifest.d.ts +93 -0
- package/dist/project-link-manifest.js +1 -0
- package/dist/registry.d.ts +30 -0
- package/dist/registry.js +1 -0
- package/dist/session-lock.d.ts +49 -0
- package/dist/session-lock.js +1 -0
- package/dist/session.d.ts +38 -0
- package/dist/session.js +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @vendure-platform/console-auth
|
|
2
|
+
|
|
3
|
+
Browser login against Vendure Console, the user-level credential store that
|
|
4
|
+
keeps the result, and the onboarding flow both tools run on top of it. Shared by
|
|
5
|
+
`@vendure-platform/create` and `@vendure-platform/cli` so there is one login and
|
|
6
|
+
one way to link a project, rather than one per tool.
|
|
7
|
+
|
|
8
|
+
This package is public and MIT because `@vendure-platform/create` runs under
|
|
9
|
+
`npx` for people who hold no registry token. It has no runtime dependencies
|
|
10
|
+
beyond `tslib`.
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { loginAndStoreConsoleToken, resolveStoredConsoleToken } from '@vendure-platform/console-auth';
|
|
16
|
+
|
|
17
|
+
const token = (await resolveStoredConsoleToken()) ?? (await loginAndStoreConsoleToken({ client: 'create' }));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`resolveStoredConsoleToken` returns a usable access token from the stored
|
|
21
|
+
credential, refreshing it first when it has expired and carries a refresh token.
|
|
22
|
+
It returns `undefined` when there is nothing usable, which is the caller's cue to
|
|
23
|
+
offer a login. The refresh runs under the cross-process lock described in
|
|
24
|
+
"Sharing the credential across processes", so two commands running at once
|
|
25
|
+
cannot spend the same single-use refresh token.
|
|
26
|
+
|
|
27
|
+
## Onboarding a project
|
|
28
|
+
|
|
29
|
+
`create` has to sign a person in, find or make a Console Project, give it an
|
|
30
|
+
edition, and write the Project Link Manifest. It runs under `npx` and cannot
|
|
31
|
+
depend on `@vendure/cli`, so that flow lives here.
|
|
32
|
+
|
|
33
|
+
`vendure console link` does none of it. `@vendure/cli` owns the link: the
|
|
34
|
+
endpoints, the approval, the browser, the polling and the manifest.
|
|
35
|
+
`@vendure-platform/cli` contributes only what comes after one, through the
|
|
36
|
+
CLI's `afterConsoleLink` hook. The manifest reader and writer here stay in step
|
|
37
|
+
with the CLI's own through `project-link-conformance.spec.ts` in that package,
|
|
38
|
+
which runs both over the same inputs.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { ConsoleApi, establishProjectLink } from '@vendure-platform/console-auth';
|
|
42
|
+
|
|
43
|
+
const api = new ConsoleApi({ apiOrigin, getAccessToken: () => token });
|
|
44
|
+
const link = await establishProjectLink({ projectRoot, api, interaction, reuseExistingLink: true });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`establishProjectLink` is the whole flow. `selectConsoleProject` and
|
|
48
|
+
`writeProjectLink` are its two halves, because `create` needs the edition while
|
|
49
|
+
its prompts are still running and cannot write a manifest until the directory
|
|
50
|
+
exists.
|
|
51
|
+
|
|
52
|
+
Every question is a call on `ConsoleOnboardingInteraction` rather than a prompt.
|
|
53
|
+
`create` draws with `@clack/prompts`, and no prompt library belongs in this
|
|
54
|
+
package. Any of
|
|
55
|
+
those calls may throw `ConsoleOnboardingCancelled`, which means the person
|
|
56
|
+
stopped and nothing should be written.
|
|
57
|
+
|
|
58
|
+
This package writes no credential of either kind:
|
|
59
|
+
|
|
60
|
+
- The account-level session is written by the login that produced the token
|
|
61
|
+
the flow runs on, which is this package's own store below.
|
|
62
|
+
- The project-scoped Environment Credential is written only by
|
|
63
|
+
`@vendure-platform/cli`. Nothing here can reach it, which is what keeps the
|
|
64
|
+
two stores apart in the module graph as well as on disk.
|
|
65
|
+
|
|
66
|
+
`acquireRegistryAccess` gets the Registry Access Token for a Project. Console
|
|
67
|
+
returns the value once, so a lost response leaves a token that exists in Console
|
|
68
|
+
and nowhere else. Creating a second one under the same name is refused.
|
|
69
|
+
The caller recovers from that refusal by finding the token of that name in the
|
|
70
|
+
list and rotating it, only after confirming, because rotating stops the old one
|
|
71
|
+
working.
|
|
72
|
+
|
|
73
|
+
## Where the credential lives
|
|
74
|
+
|
|
75
|
+
`~/.vendure/console-credentials.json`, directory mode `0700`, file mode `0600`,
|
|
76
|
+
written through a temporary file and a rename so a reader never sees a
|
|
77
|
+
half-written token.
|
|
78
|
+
|
|
79
|
+
The repository has no `XDG_CONFIG_HOME` precedent, so this does not invent one.
|
|
80
|
+
|
|
81
|
+
This is an **account-level** credential and it is deliberately not the same thing
|
|
82
|
+
as the per-project development credential `@vendure-platform/cli` keeps in
|
|
83
|
+
`<projectRoot>/.vendure/credentials.json`. That one authenticates one
|
|
84
|
+
environment of one project. This one is worth as much as the person's Console
|
|
85
|
+
password across every project they own, so it never goes near a project
|
|
86
|
+
directory that someone is about to `git init`.
|
|
87
|
+
|
|
88
|
+
A missing file, an unreadable one, malformed JSON, or a credential issued by a
|
|
89
|
+
different Console app or API origin all read as "no credential". A corrupt cache is not worth
|
|
90
|
+
stopping a scaffold over.
|
|
91
|
+
|
|
92
|
+
Schema version 2 records the issuing API origin. Reads, refreshes, and login
|
|
93
|
+
validate and normalize both origins. A token is reused only when both origins
|
|
94
|
+
match. Schema version 1 has no known API issuer and requires a new login.
|
|
95
|
+
Logging in against another API replaces the single cache entry, but that entry
|
|
96
|
+
cannot be reused against production.
|
|
97
|
+
|
|
98
|
+
Only one thing removes a stored credential: a refresh that Console refused,
|
|
99
|
+
observed under the lock described below, against a refresh token that is still
|
|
100
|
+
the stored one. There is still no `vendure console logout` command, because
|
|
101
|
+
nothing calls for one today.
|
|
102
|
+
|
|
103
|
+
## Sharing the credential across processes
|
|
104
|
+
|
|
105
|
+
Console rotates the refresh token on use and refuses the spent one. The second
|
|
106
|
+
process to present the same token is left with nothing, and if it then writes
|
|
107
|
+
its own stale pair back it overwrites a live session with a dead one. Two
|
|
108
|
+
processes are ordinary: `vendure dev` runs for hours while other commands run in
|
|
109
|
+
another terminal, and Vendure Cloud commands read the same file.
|
|
110
|
+
|
|
111
|
+
So every step that spends a refresh token or rewrites the credential goes
|
|
112
|
+
through one lock, because the double spend stays reachable through any step
|
|
113
|
+
left out. `resolveStoredConsoleToken`, `refreshStoredConsoleToken` and
|
|
114
|
+
`loginAndStoreConsoleToken` take it for you and are the whole of what this
|
|
115
|
+
package writes credentials from. `writeConsoleCredential` is exported so a test can
|
|
116
|
+
put a credential in place, and it takes no lock: any other caller has to wrap it
|
|
117
|
+
in `withConsoleSessionLock`.
|
|
118
|
+
|
|
119
|
+
Vendure Cloud implements this from its own repository and its own release train.
|
|
120
|
+
The protocol is written down here rather than shared as code, because the two
|
|
121
|
+
sides have only the filesystem in common. TypeScript callers import
|
|
122
|
+
`withConsoleSessionLock`.
|
|
123
|
+
|
|
124
|
+
### The lock file
|
|
125
|
+
|
|
126
|
+
`~/.vendure/console-credentials.lock`, beside the credential it guards.
|
|
127
|
+
|
|
128
|
+
Take the lock by creating that file with `O_CREAT | O_EXCL` at mode `0600`
|
|
129
|
+
(`wx` in Node). The create either wins or fails with `EEXIST`, with no window in
|
|
130
|
+
between, and that is the whole mechanism. Write one line of JSON into it:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "token": "3f0c1a6e-9c2b-4d51-9f0a-2e7c5b1d8a44", "pid": 4242, "acquiredAt": "2026-01-01T12:00:00.000Z" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`token` is new for every acquisition and is what makes releasing safe. `pid` and
|
|
137
|
+
`acquiredAt` are for a person looking at a lock that will not go away.
|
|
138
|
+
|
|
139
|
+
Release the lock by reading the file and removing it only while `token` still
|
|
140
|
+
matches the one you wrote. A lock carrying another token was broken as stale and
|
|
141
|
+
taken over while you were working, and removing it would let a third process in
|
|
142
|
+
while the second one writes.
|
|
143
|
+
|
|
144
|
+
The lock is not reentrant. `resolveStoredConsoleToken`, `refreshStoredConsoleToken`
|
|
145
|
+
and `loginAndStoreConsoleToken` take it themselves, so wrapping one of them in
|
|
146
|
+
`withConsoleSessionLock` waits out the whole wait budget and then fails: the
|
|
147
|
+
outer lock is held by a process that is alive and beating, which is the one
|
|
148
|
+
thing a waiter never breaks.
|
|
149
|
+
|
|
150
|
+
### The budgets
|
|
151
|
+
|
|
152
|
+
| Budget | Value | Meaning |
|
|
153
|
+
| --------- | ----- | ------------------------------------------------------------------ |
|
|
154
|
+
| Stale | 30s | A lock whose mtime is older than this may be taken over by anyone. |
|
|
155
|
+
| Wait | 30s | How long to wait for a lock somebody else holds before giving up. |
|
|
156
|
+
| Heartbeat | 10s | How often a holder touches its own lock while it works. |
|
|
157
|
+
|
|
158
|
+
Never make the wait budget shorter than the stale budget. A waiter that gave up
|
|
159
|
+
sooner could give up while the holder was dead but not yet stale, and then "I
|
|
160
|
+
gave up waiting" would stop meaning "the holder is gone".
|
|
161
|
+
|
|
162
|
+
Equal budgets only make them the same condition if both are measured against
|
|
163
|
+
the same clock. A lock mtime carries a fraction of a millisecond that
|
|
164
|
+
`Date.now()` does not, so compare whole milliseconds. Look at the lock once more
|
|
165
|
+
when the budget runs out, too: it can cross the stale boundary while the check
|
|
166
|
+
before it is still reading.
|
|
167
|
+
|
|
168
|
+
Touch your own lock while you hold it, a third of the stale budget apart. That
|
|
169
|
+
is what makes the stale budget measure a holder that has stopped rather than one
|
|
170
|
+
that is slow: without it, a hold that runs long on a loaded machine is broken
|
|
171
|
+
while it is still working, and two processes refresh at once. Two missed beats
|
|
172
|
+
are allowed before anyone may act on the silence. A participant that does not
|
|
173
|
+
beat is not broken by this, it is only judged on how long its work takes, which
|
|
174
|
+
is what every participant was judged on before.
|
|
175
|
+
|
|
176
|
+
Bound that work anyway. The longest legitimate hold is the two token requests a
|
|
177
|
+
refresh makes when the first is refused and a rotated pair is on disk to try
|
|
178
|
+
instead, and each request has to carry a deadline that covers reading the answer
|
|
179
|
+
as well as getting it, or a server that sends headers and then stops sending
|
|
180
|
+
holds the lock for as long as it likes. This package gives each request 10
|
|
181
|
+
seconds, so the work inside a lock is 20 seconds at the outside.
|
|
182
|
+
|
|
183
|
+
Take a stale lock over by renaming it aside to a name of your own, not by
|
|
184
|
+
removing it in place. `rename` is one operation, so one waiter moves the file
|
|
185
|
+
and the rest find it gone and go back to creating. Removing it by path lets a
|
|
186
|
+
second waiter, still holding its reading of a file the first waiter has already
|
|
187
|
+
removed, delete the fresh lock the first waiter created in its place, and then
|
|
188
|
+
two processes hold the lock at once. Read what you moved: if it is not the lock
|
|
189
|
+
you judged stale, somebody took the lock in between, so `link` it back where it
|
|
190
|
+
was and leave it alone.
|
|
191
|
+
|
|
192
|
+
A window remains between reading the lock and moving it, so treat this as
|
|
193
|
+
narrowed rather than closed. A lock taken inside that window is moved aside by
|
|
194
|
+
somebody who judged the lock before it, and putting it back fails if a third
|
|
195
|
+
lock has landed by then, which leaves two holders. The recovery below is what
|
|
196
|
+
makes that survivable: two holders spend one grant between them and the loser
|
|
197
|
+
reuses what the winner wrote, so the cost is a wasted request rather than a
|
|
198
|
+
lost session.
|
|
199
|
+
|
|
200
|
+
While you hold the lock, read the credential again. Waiting for the lock is the
|
|
201
|
+
usual way to find out that another process has refreshed: by the time you get
|
|
202
|
+
in, the pair you read beforehand is the stale copy and the one on disk is live.
|
|
203
|
+
A credential that is usable when read under the lock is the answer, and no grant
|
|
204
|
+
needs spending.
|
|
205
|
+
|
|
206
|
+
### When Console refuses the refresh
|
|
207
|
+
|
|
208
|
+
Re-read the credential before believing the refusal. A single-use refresh token
|
|
209
|
+
is usually refused because another process spent it first, and the pair that
|
|
210
|
+
process got back is already on disk. Use that pair.
|
|
211
|
+
|
|
212
|
+
If the stored refresh token is still the one that was just refused, and the lock
|
|
213
|
+
was held across both the refusal and the read, then nothing can have rotated it
|
|
214
|
+
and the session really is over. Only there may the credential be removed. Never
|
|
215
|
+
remove or overwrite it on the strength of a refusal observed without the lock.
|
|
216
|
+
|
|
217
|
+
Tell a refusal apart from a failure, and read the body to do it. Console names a
|
|
218
|
+
refused grant with `"code": "cli_session.invalid_grant"`. A 4xx that does not
|
|
219
|
+
name one is somebody other than Console answering: an API origin pointed at the
|
|
220
|
+
wrong host returning 404, a proxy in front of Console returning 403, a gateway
|
|
221
|
+
that wants a credential of its own returning 401. A timeout, an unreachable
|
|
222
|
+
host, a 5xx and a malformed response say nothing about the refresh token
|
|
223
|
+
either. All of them leave the credential untouched, and the next attempt gets a
|
|
224
|
+
straight answer.
|
|
225
|
+
|
|
226
|
+
When you cannot take the lock, do nothing: no refresh, no write, no removal.
|
|
227
|
+
Because the wait budget covers the stale budget, failing to acquire means the
|
|
228
|
+
filesystem refused you or other processes kept taking the lock first. Neither
|
|
229
|
+
says anything about the session, so report it as a temporary failure and leave
|
|
230
|
+
the credential where it is. Reporting it as "no credential" is not that, because
|
|
231
|
+
the caller answers that by sending a person to the browser to log in over a
|
|
232
|
+
session that is fine.
|
|
233
|
+
|
|
234
|
+
## Trusted origins
|
|
235
|
+
|
|
236
|
+
Production pairs `https://console.vendure.io` with `https://api.vendure.io`.
|
|
237
|
+
Staging pairs `https://staging.console.vendure.io` with `https://staging.api.vendure.io`.
|
|
238
|
+
Supplying only the staging API selects the staging app for browser login and
|
|
239
|
+
cache lookup. Supplying only the staging app selects its API. Explicit mixed
|
|
240
|
+
production and staging pairs are rejected. Loopback hosts remain allowed for
|
|
241
|
+
local development and tests.
|
|
242
|
+
|
|
243
|
+
Any URL carrying a path, a query, a fragment, or credentials is rejected before
|
|
244
|
+
use, and every request sets `redirect: 'error'`. A host allow-list on its own is
|
|
245
|
+
not enough: those are the shapes that let a URL send the browser somewhere else
|
|
246
|
+
while the hostname still reads correctly.
|
|
247
|
+
|
|
248
|
+
## Console frontend contract
|
|
249
|
+
|
|
250
|
+
> Console serves all of this as of Console #121. The route is
|
|
251
|
+
> `apps/console/src/routes/cli-auth.tsx` and the token endpoint is
|
|
252
|
+
> `apps/api/src/cli-sessions/cli-auth.controller.ts`. What is written down here
|
|
253
|
+
> is what both sides implement, so a change on either side changes this file.
|
|
254
|
+
|
|
255
|
+
The user logs in on the Console web app, not on the API and not on an identity
|
|
256
|
+
provider this CLI picked. That way the address bar shows a host they already
|
|
257
|
+
trust.
|
|
258
|
+
|
|
259
|
+
### `GET https://console.vendure.io/cli-auth`
|
|
260
|
+
|
|
261
|
+
| Query param | Required | Meaning |
|
|
262
|
+
| ----------------------- | -------- | -------------------------------------------------------------- |
|
|
263
|
+
| `client` | yes | `create` or `cli`. Console names the tool on the confirm page. |
|
|
264
|
+
| `redirect_uri` | yes | `http://127.0.0.1:<port>/auth/callback`. Loopback only. |
|
|
265
|
+
| `state` | yes | Opaque, 32 random bytes base64url. Returned verbatim. |
|
|
266
|
+
| `code_challenge` | yes | PKCE challenge, base64url SHA-256 of the verifier. |
|
|
267
|
+
| `code_challenge_method` | yes | Always `S256`. |
|
|
268
|
+
|
|
269
|
+
Console behaviour:
|
|
270
|
+
|
|
271
|
+
1. If the visitor is not signed in, show the normal Console login first.
|
|
272
|
+
2. Show a confirmation page naming the tool from `client` and the account that
|
|
273
|
+
will be authorized.
|
|
274
|
+
3. On approval, mint a short-lived single-use authorization code bound to the
|
|
275
|
+
account and to `code_challenge`, then `302` to
|
|
276
|
+
`redirect_uri?code=<code>&state=<state>`.
|
|
277
|
+
4. On refusal, `302` to `redirect_uri?error=access_denied&state=<state>`.
|
|
278
|
+
|
|
279
|
+
Console must reject any `redirect_uri` that is not loopback. That check is what
|
|
280
|
+
stops an attacker sending a code somewhere else.
|
|
281
|
+
|
|
282
|
+
### `POST https://api.vendure.io/v1/auth/cli/token`
|
|
283
|
+
|
|
284
|
+
Body for the initial exchange:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"grant_type": "authorization_code",
|
|
289
|
+
"code": "...",
|
|
290
|
+
"code_verifier": "...",
|
|
291
|
+
"redirect_uri": "http://127.0.0.1:54321/auth/callback"
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Body for a refresh:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{ "grant_type": "refresh_token", "refresh_token": "..." }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Response:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"access_token": "...",
|
|
306
|
+
"token_type": "Bearer",
|
|
307
|
+
"expires_in": 3600,
|
|
308
|
+
"refresh_token": "..."
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`refresh_token` is optional. `expires_in` is required: a session assumed to last
|
|
313
|
+
longer than it does fails later, at the project lookup, where the cause is no
|
|
314
|
+
longer visible.
|
|
315
|
+
|
|
316
|
+
A refresh token that has already been spent, expired or been revoked is refused
|
|
317
|
+
with `400` and the Console error convention, which names the condition under
|
|
318
|
+
`code` rather than under the OAuth `error`:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{ "code": "cli_session.invalid_grant", "message": "This refresh token was already used." }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
That `code` is the one thing that ends a session. It is the only code the token
|
|
325
|
+
endpoint refuses with, so it covers a token request Console could not read at
|
|
326
|
+
all as well as a grant it will not honour again. On the refresh path that
|
|
327
|
+
distinction does not arise: the request carries `grant_type` and
|
|
328
|
+
`refresh_token`, both well formed, so the only thing left for Console to refuse
|
|
329
|
+
is the grant. Any other status or body reads as a request that failed rather
|
|
330
|
+
than a grant that was judged, and leaves the stored credential alone. `message`
|
|
331
|
+
is for a person; nothing branches on it.
|
|
332
|
+
|
|
333
|
+
The `access_token` is sent as `Authorization: Bearer <token>` to
|
|
334
|
+
`GET /v1/projects/editions`, which Console serves from
|
|
335
|
+
`apps/api/src/projects/projects.controller.ts`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Identifies the calling tool so Console can name it on the confirmation page. */
|
|
2
|
+
export type ConsoleAuthClient = 'create' | 'cli';
|
|
3
|
+
export interface ConsoleSession {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
/** Absolute expiry, ISO-8601. Derived from the `expires_in` Console returns. */
|
|
6
|
+
expiresAt: string;
|
|
7
|
+
refreshToken?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A token grant that did not produce a session.
|
|
11
|
+
*
|
|
12
|
+
* `refused` separates "Console judged this grant and said no" from "ask again
|
|
13
|
+
* later", and only a refusal may end a session. A 503, a timeout, an
|
|
14
|
+
* unreachable host and a malformed answer all say nothing about whether the
|
|
15
|
+
* refresh token is still good, so acting on one would log a person out because
|
|
16
|
+
* their network dropped.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ConsoleTokenGrantError extends Error {
|
|
19
|
+
readonly refused: boolean;
|
|
20
|
+
constructor(message: string, refused: boolean);
|
|
21
|
+
}
|
|
22
|
+
export interface BrowserLoginOptions {
|
|
23
|
+
client: ConsoleAuthClient;
|
|
24
|
+
appOrigin?: string;
|
|
25
|
+
apiOrigin?: string;
|
|
26
|
+
fetch?: typeof globalThis.fetch;
|
|
27
|
+
openBrowser?: (url: string) => Promise<boolean>;
|
|
28
|
+
reportAuthorizationUrl?: (url: string) => void;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Log in through the Vendure Console web app and return an account-level
|
|
35
|
+
* session.
|
|
36
|
+
*
|
|
37
|
+
* The browser only ever visits Console. A loopback server on 127.0.0.1 receives
|
|
38
|
+
* the authorization code, and the code is traded for a token by this process
|
|
39
|
+
* over HTTPS, so the token never travels through the URL bar or the shell
|
|
40
|
+
* history.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loginWithBrowser(options: BrowserLoginOptions): Promise<ConsoleSession>;
|
|
43
|
+
/** Trade a refresh token for a fresh session. Same contract as the code exchange. */
|
|
44
|
+
export declare function refreshSession(refreshToken: string, options?: {
|
|
45
|
+
apiOrigin?: string;
|
|
46
|
+
fetch?: typeof globalThis.fetch;
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
}): Promise<ConsoleSession>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ConsoleTokenGrantError=void 0,exports.loginWithBrowser=d,exports.refreshSession=h;const e=require("node:child_process"),r=require("node:crypto"),t=require("node:http"),n=require("./origins"),o="127.0.0.1",i="/auth/callback",a="/cli-auth",s="/v1/auth/cli/token",c=3e5,l=1e4,u=31536e3;class ConsoleTokenGrantError extends Error{constructor(e,r){super(e),this.name="ConsoleTokenGrantError",this.refused=r}}async function d(e){var r,a,s,l,u;const{appOrigin:d,apiOrigin:h}=(0,n.consoleOrigins)(e),w=null!==(r=e.fetch)&&void 0!==r?r:globalThis.fetch,g=(0,t.createServer)();try{const r=await T(g),t=`http://${o}:${r}${i}`,n=k(),m=k(),v=p({appOrigin:d,client:e.client,redirectUri:t,state:n,codeVerifier:m}),y=_(g,n,{signal:e.signal,timeoutMs:null!==(a=e.timeoutMs)&&void 0!==a?a:c});y.catch(()=>{}),await(null!==(s=e.openBrowser)&&void 0!==s?s:E)(v).catch(()=>!1)||null===(l=e.reportAuthorizationUrl)||void 0===l||l.call(e,v);const b=await y;return await f({apiOrigin:h,fetch:w,code:b,codeVerifier:m,redirectUri:t,signal:e.signal,now:null!==(u=e.now)&&void 0!==u?u:()=>new Date})}finally{await b(g)}}async function h(e,r={}){var t,o;const i=(0,n.consoleApiOrigin)(r.apiOrigin);return v(await w(i,null!==(t=r.fetch)&&void 0!==t?t:globalThis.fetch,{grant_type:"refresh_token",refresh_token:e},void 0),null!==(o=r.now)&&void 0!==o?o:()=>new Date,e)}function p(e){const r=new URL(a,e.appOrigin);return r.searchParams.set("client",e.client),r.searchParams.set("redirect_uri",e.redirectUri),r.searchParams.set("state",e.state),r.searchParams.set("code_challenge",y(e.codeVerifier)),r.searchParams.set("code_challenge_method","S256"),r.toString()}async function f(e){return v(await w(e.apiOrigin,e.fetch,{grant_type:"authorization_code",code:e.code,code_verifier:e.codeVerifier,redirect_uri:e.redirectUri},e.signal),e.now)}async function w(e,r,t,n){const o=new AbortController,i=()=>o.abort();null==n||n.addEventListener("abort",i,{once:!0});const a=setTimeout(i,l);try{let n;try{n=await r(`${e}${s}`,{method:"POST",redirect:"error",signal:o.signal,headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(t)})}catch{throw new ConsoleTokenGrantError("Could not reach Vendure Console for a token.",!1)}if(!n.ok)throw new ConsoleTokenGrantError(`Vendure Console rejected the token request with HTTP ${n.status}.`,await m(n));try{return await n.json()}catch{throw new ConsoleTokenGrantError("Vendure Console returned malformed JSON for a token.",!1)}}finally{clearTimeout(a),null==n||n.removeEventListener("abort",i)}}exports.ConsoleTokenGrantError=ConsoleTokenGrantError;const g="cli_session.invalid_grant";async function m(e){if(e.status<400||500<=e.status)return!1;try{const{code:r}=JSON.parse(await e.text());return r===g}catch{return!1}}function v(e,r,t){const n=e;if("string"!=typeof(null==n?void 0:n.access_token)||!n.access_token)throw new Error("Vendure Console returned no access token.");if("Bearer"!==n.token_type)throw new Error(`Vendure Console returned an unsupported token type "${String(n.token_type)}".`);if("number"!=typeof n.expires_in||!Number.isInteger(n.expires_in)||n.expires_in<=0||n.expires_in>u)throw new Error("Vendure Console returned no usable token expiry.");const o="string"==typeof n.refresh_token&&n.refresh_token?n.refresh_token:t;return{accessToken:n.access_token,expiresAt:new Date(r().getTime()+1e3*n.expires_in).toISOString(),...o?{refreshToken:o}:{}}}function k(){return(0,r.randomBytes)(32).toString("base64url")}function y(e){return(0,r.createHash)("sha256").update(e).digest("base64url")}function _(e,r,t){return new Promise((n,a)=>{var s,c;let l=!1;const u=e=>{var r;l||(l=!0,clearTimeout(h),null===(r=t.signal)||void 0===r||r.removeEventListener("abort",d),"error"in e?a(e.error):n(e.code))},d=()=>u({error:new Error("Login was interrupted.")}),h=setTimeout(()=>u({error:new Error("Login timed out.")}),t.timeoutMs);null===(s=t.signal)||void 0===s||s.addEventListener("abort",d,{once:!0}),(null===(c=t.signal)||void 0===c?void 0:c.aborted)&&d(),e.on("request",(e,t)=>{var n;const a=new URL(null!==(n=e.url)&&void 0!==n?n:"/",`http://${o}`);if("GET"!==e.method||a.pathname!==i)return void t.writeHead(404).end("Not found");if(a.searchParams.get("state")!==r)return t.writeHead(400).end("Login state did not match. Return to the terminal."),void u({error:new Error("Login state did not match.")});const s=a.searchParams.get("code");if(!s||a.searchParams.has("error"))return t.writeHead(400).end("Login was not completed. Return to the terminal."),void u({error:new Error("Vendure Console did not authorize the login.")});t.writeHead(200,{"Content-Type":"text/plain; charset=utf-8"}),t.end("Login complete. You can close this window."),u({code:s})})})}function T(e){return new Promise((r,t)=>{e.once("error",t),e.listen(0,o,()=>{e.off("error",t);const n=e.address();n&&"string"!=typeof n?r(n.port):t(new Error("Could not start the local login callback."))})})}function b(e){return e.listening?new Promise(r=>e.close(()=>r())):Promise.resolve()}function E(r){const t="darwin"===process.platform?{executable:"open",args:[r]}:"win32"===process.platform?{executable:"rundll32",args:["url.dll,FileProtocolHandler",r]}:{executable:"xdg-open",args:[r]};return new Promise(r=>{const n=(0,e.spawn)(t.executable,t.args,{detached:!0,stdio:"ignore"});n.once("error",()=>r(!1)),n.once("spawn",()=>{n.unref(),r(!0)})})}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console answered, and the answer was a refusal. `code` carries the Console
|
|
3
|
+
* error convention (`project.not_found`, `auth.permission_denied`, and so on),
|
|
4
|
+
* which is what callers branch on: the status alone cannot tell a missing
|
|
5
|
+
* permission from a missing Project.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ConsoleApiError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly code: string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The parsed error body. Console puts the actionable part of a refusal
|
|
12
|
+
* beside `code`: an Evaluation denial carries `reason` and
|
|
13
|
+
* `nextEligibleAt`, and neither survives being flattened into a string.
|
|
14
|
+
*/
|
|
15
|
+
readonly body?: Record<string, unknown>;
|
|
16
|
+
constructor(status: number, code: string | undefined, message: string,
|
|
17
|
+
/**
|
|
18
|
+
* The parsed error body. Console puts the actionable part of a refusal
|
|
19
|
+
* beside `code`: an Evaluation denial carries `reason` and
|
|
20
|
+
* `nextEligibleAt`, and neither survives being flattened into a string.
|
|
21
|
+
*/
|
|
22
|
+
body?: Record<string, unknown>);
|
|
23
|
+
}
|
|
24
|
+
/** The request never produced an answer. Says nothing about the request itself. */
|
|
25
|
+
export declare class ConsoleApiTransportError extends Error {
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
export interface ConsoleApiOptions {
|
|
29
|
+
/** Validated through the shared origin allow-list before anything is sent. */
|
|
30
|
+
apiOrigin?: string;
|
|
31
|
+
getAccessToken: () => string | Promise<string>;
|
|
32
|
+
/** Called once per client when Console reports the access token expired. */
|
|
33
|
+
refreshAccessToken?: () => void | Promise<void>;
|
|
34
|
+
fetch?: typeof globalThis.fetch;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ConsoleApiRequestOptions {
|
|
39
|
+
method?: 'GET' | 'POST';
|
|
40
|
+
body?: unknown;
|
|
41
|
+
/** Extra request headers. `Idempotency-Key` is the only one in use. */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The authenticated Console API calls the onboarding flow makes.
|
|
46
|
+
*
|
|
47
|
+
* `@vendure-platform/license-guard` carries a client of its own for the
|
|
48
|
+
* entitlement runtime. This one is not that one and does not import it: that
|
|
49
|
+
* package declares a peer dependency on `@vendure/core`, and
|
|
50
|
+
* `@vendure-platform/create` runs under `npx` in a directory that has no
|
|
51
|
+
* Vendure installed. It also has to send `Idempotency-Key`, which the runtime
|
|
52
|
+
* client has no reason to.
|
|
53
|
+
*/
|
|
54
|
+
export declare class ConsoleApi {
|
|
55
|
+
private readonly options;
|
|
56
|
+
readonly apiOrigin: string;
|
|
57
|
+
private readonly fetch;
|
|
58
|
+
private refreshed;
|
|
59
|
+
constructor(options: ConsoleApiOptions);
|
|
60
|
+
request<T>(path: string, options?: ConsoleApiRequestOptions): Promise<T>;
|
|
61
|
+
private send;
|
|
62
|
+
}
|
|
63
|
+
/** Whether Console refused because the session lacks the Permission. */
|
|
64
|
+
export declare function isPermissionDenied(error: unknown): boolean;
|
|
65
|
+
/** Whether Console refused with HTTP 409 and this error code. */
|
|
66
|
+
export declare function isConsoleConflict(error: unknown, code: string): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ConsoleApi=exports.ConsoleApiTransportError=exports.ConsoleApiError=void 0,exports.isPermissionDenied=s,exports.isConsoleConflict=t;const o=require("./origins");class ConsoleApiError extends Error{constructor(o,r,e,s){super(e),this.status=o,this.code=r,this.body=s,this.name="ConsoleApiError"}}exports.ConsoleApiError=ConsoleApiError;class ConsoleApiTransportError extends Error{constructor(o){super(o),this.name="ConsoleApiTransportError"}}exports.ConsoleApiTransportError=ConsoleApiTransportError;const r=1e4,e=65536;class ConsoleApi{constructor(r){var e;this.options=r,this.refreshed=!1,this.apiOrigin=(0,o.consoleApiOrigin)(r.apiOrigin),this.fetch=null!==(e=r.fetch)&&void 0!==e?e:globalThis.fetch}async request(o,r={}){try{return await this.send(o,r)}catch(e){if(e instanceof ConsoleApiError&&401===e.status&&"auth.token_expired"===e.code&&!this.refreshed&&this.options.refreshAccessToken)return this.refreshed=!0,await this.options.refreshAccessToken(),this.send(o,r);throw e}}async send(o,e){var s,t,a,l,p;if(!o.startsWith("/")||o.startsWith("//"))throw new Error("The Vendure Console API path must start with one slash.");const c=await this.options.getAccessToken();if(!c)throw new Error("No Vendure Console session is available.");const d=new AbortController,h=()=>d.abort();null===(s=this.options.signal)||void 0===s||s.addEventListener("abort",h,{once:!0});const u=setTimeout(h,null!==(t=this.options.timeoutMs)&&void 0!==t?t:r);try{const r=await this.fetch(`${this.apiOrigin}${o}`,{method:null!==(a=e.method)&&void 0!==a?a:void 0===e.body?"GET":"POST",redirect:"error",signal:d.signal,headers:{Authorization:`Bearer ${c}`,Accept:"application/json",...void 0===e.body?{}:{"Content-Type":"application/json"},...e.headers},...void 0===e.body?{}:{body:JSON.stringify(e.body)}}),s=await n(r);if(!r.ok){const{code:o,message:e,fields:t}=i(s);throw new ConsoleApiError(r.status,o,null!=e?e:`Vendure Console returned HTTP ${r.status}.`,t)}return s}catch(o){if(o instanceof ConsoleApiError)throw o;if(null===(l=this.options.signal)||void 0===l?void 0:l.aborted)throw new ConsoleApiTransportError("The request was cancelled.");if(o instanceof Error&&"AbortError"===o.name)throw new ConsoleApiTransportError("The Vendure Console request timed out.");throw new ConsoleApiTransportError("Could not reach Vendure Console.")}finally{clearTimeout(u),null===(p=this.options.signal)||void 0===p||p.removeEventListener("abort",h)}}}function s(o){return o instanceof ConsoleApiError&&403===o.status&&"auth.permission_denied"===o.code}function t(o,r){return o instanceof ConsoleApiError&&409===o.status&&o.code===r}async function n(o){const r=await o.text();if(Buffer.byteLength(r,"utf8")>e)throw new ConsoleApiError(o.status,void 0,"The Vendure Console response was too large.");if(r)try{return JSON.parse(r)}catch{throw new ConsoleApiError(o.status,void 0,"Vendure Console returned malformed JSON.")}}function i(o){if(!o||"object"!=typeof o||Array.isArray(o))return{};const r=o;return{code:"string"==typeof r.code?r.code:void 0,message:"string"==typeof r.message?r.message:void 0,fields:r}}exports.ConsoleApi=ConsoleApi;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ConsoleApi } from './console-api';
|
|
2
|
+
import { type ProjectLinkManifest } from './project-link-manifest';
|
|
3
|
+
/** Product Offer behind a Project's current access. */
|
|
4
|
+
export type ProjectEdition = 'starter' | 'growth' | 'scale' | 'enterprise';
|
|
5
|
+
/** Where that access came from. `none` means the Project has no access at all. */
|
|
6
|
+
export type ProjectEditionSource = 'override' | 'subscription' | 'evaluation' | 'none';
|
|
7
|
+
/** The Offers an Evaluation may be started on. Enterprise is sold, never evaluated. */
|
|
8
|
+
export type EvaluationOfferCode = 'starter' | 'growth' | 'scale';
|
|
9
|
+
export declare const PROJECT_EDITIONS: readonly ProjectEdition[];
|
|
10
|
+
export declare const EVALUATION_OFFER_CODES: readonly EvaluationOfferCode[];
|
|
11
|
+
/**
|
|
12
|
+
* One ACTIVE Project of the signed-in account with the edition it currently
|
|
13
|
+
* holds. `edition` is null when no Offer supplies the access, which a support
|
|
14
|
+
* override granting a bare Capability list produces, so a null edition is a
|
|
15
|
+
* Project that exists rather than one that is broken.
|
|
16
|
+
*/
|
|
17
|
+
export interface ConsoleProjectEdition {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
edition: ProjectEdition | null;
|
|
21
|
+
source: ProjectEditionSource;
|
|
22
|
+
}
|
|
23
|
+
export interface ConsoleProject {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
state: 'active' | 'archived';
|
|
27
|
+
}
|
|
28
|
+
export interface ProjectEvaluationState {
|
|
29
|
+
startState: 'available' | 'already_active' | 'project_inactive' | 'account_evaluation_active' | 'rolling_window';
|
|
30
|
+
nextEligibleAt: string | null;
|
|
31
|
+
offerCodes: EvaluationOfferCode[];
|
|
32
|
+
}
|
|
33
|
+
export interface StartedEvaluation {
|
|
34
|
+
outcome: 'started' | 'existing';
|
|
35
|
+
offerCode: string;
|
|
36
|
+
expiresAt: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RegistryAccessConfiguration {
|
|
39
|
+
registryUrl: string;
|
|
40
|
+
scope: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RegistryAccessToken {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
state: 'active' | 'expired' | 'revoked';
|
|
46
|
+
}
|
|
47
|
+
export interface IssuedRegistryAccessToken extends RegistryAccessToken {
|
|
48
|
+
/** Returned once, by the create and rotate routes only. */
|
|
49
|
+
token: string;
|
|
50
|
+
}
|
|
51
|
+
/** Every ACTIVE Project of the account, with its edition. */
|
|
52
|
+
export declare function listProjectEditions(api: ConsoleApi): Promise<ConsoleProjectEdition[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Create a Project.
|
|
55
|
+
*
|
|
56
|
+
* `idempotencyKey` makes a repeat safe after a lost response: the same key and
|
|
57
|
+
* the same name return the Project the first request created rather than a
|
|
58
|
+
* second one. Console answers a repeat that carries a different name with
|
|
59
|
+
* `project.idempotency_conflict`, which is a caller bug rather than a race, so
|
|
60
|
+
* it surfaces as-is.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createProject(api: ConsoleApi, input: {
|
|
63
|
+
name: string;
|
|
64
|
+
idempotencyKey?: string;
|
|
65
|
+
}): Promise<ConsoleProject>;
|
|
66
|
+
export declare function getProjectEvaluation(api: ConsoleApi, projectId: string): Promise<ProjectEvaluationState>;
|
|
67
|
+
/**
|
|
68
|
+
* Start an Evaluation.
|
|
69
|
+
*
|
|
70
|
+
* Retry-safe: `outcome` reports `existing` when the Project already had the
|
|
71
|
+
* Evaluation this call would have created, so a repeat after a lost response
|
|
72
|
+
* does not need to be distinguished from the first attempt.
|
|
73
|
+
*/
|
|
74
|
+
export declare function startProjectEvaluation(api: ConsoleApi, projectId: string, offerCode: EvaluationOfferCode): Promise<StartedEvaluation>;
|
|
75
|
+
/**
|
|
76
|
+
* Why Console refused to start an Evaluation, or `undefined` when the failure
|
|
77
|
+
* was something else. `nextEligibleAt` is the only part a person can act on:
|
|
78
|
+
* it names the date the rolling window reopens.
|
|
79
|
+
*/
|
|
80
|
+
export declare function evaluationDenial(error: unknown): {
|
|
81
|
+
message: string;
|
|
82
|
+
reason?: string;
|
|
83
|
+
nextEligibleAt?: string;
|
|
84
|
+
} | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Create an approved Project Link for a Project the signed-in session already
|
|
87
|
+
* owns, and return its Manifest.
|
|
88
|
+
*
|
|
89
|
+
* This is the one-call route, not the browser approval flow: the caller holds
|
|
90
|
+
* the person's own Console session, so there is nobody left to ask.
|
|
91
|
+
*/
|
|
92
|
+
export declare function linkProject(api: ConsoleApi, projectId: string): Promise<ProjectLinkManifest>;
|
|
93
|
+
export declare function getRegistryAccess(api: ConsoleApi, projectId: string): Promise<RegistryAccessConfiguration>;
|
|
94
|
+
export declare function listRegistryAccessTokens(api: ConsoleApi, projectId: string): Promise<RegistryAccessToken[]>;
|
|
95
|
+
export declare function createRegistryAccessToken(api: ConsoleApi, projectId: string, name: string): Promise<IssuedRegistryAccessToken>;
|
|
96
|
+
export declare function rotateRegistryAccessToken(api: ConsoleApi, projectId: string, tokenId: string): Promise<IssuedRegistryAccessToken>;
|
|
97
|
+
/** The Customer Account the session is scoped to, or `undefined` when it is scoped to none. */
|
|
98
|
+
export declare function readViewerAccount(api: ConsoleApi): Promise<{
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
} | undefined>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.EVALUATION_OFFER_CODES=exports.PROJECT_EDITIONS=void 0,exports.listProjectEditions=r,exports.createProject=o,exports.getProjectEvaluation=n,exports.startProjectEvaluation=s,exports.evaluationDenial=a,exports.linkProject=i,exports.getRegistryAccess=c,exports.listRegistryAccessTokens=u,exports.createRegistryAccessToken=l,exports.rotateRegistryAccessToken=d,exports.readViewerAccount=y;const e=require("./console-api"),t=require("./project-link-manifest");async function r(e){const t=await e.request("/v1/projects/editions");if(!Array.isArray(t))throw x("the Project list");return t.map(e=>{const t=g(e,"a Project");return{id:v(t.id,"a Project id"),name:v(t.name,"a Project name"),edition:E(t.edition,exports.PROJECT_EDITIONS,"a Project edition"),source:A(t.source,["override","subscription","evaluation","none"],"a Project access source")}})}async function o(e,t){const r=g(await e.request("/v1/projects",{method:"POST",body:{name:t.name},...t.idempotencyKey?{headers:{"Idempotency-Key":t.idempotencyKey}}:{}}),"the created Project");return{id:v(r.id,"a Project id"),name:v(r.name,"a Project name"),state:A(r.state,["active","archived"],"a Project state")}}async function n(e,t){const r=g(await e.request(`${f(t)}/evaluation`),"the Evaluation state");if(!Array.isArray(r.offerCodes))throw x("the evaluable Offer list");return{startState:A(r.startState,["available","already_active","project_inactive","account_evaluation_active","rolling_window"],"an Evaluation start state"),nextEligibleAt:null===r.nextEligibleAt?null:v(r.nextEligibleAt,"a next eligible time"),offerCodes:r.offerCodes.filter(e=>exports.EVALUATION_OFFER_CODES.includes(e))}}async function s(e,t,r){const o=g(await e.request(`${f(t)}/evaluation`,{method:"POST",body:{offerCode:r}}),"the Evaluation result"),n=g(o.evaluation,"the Evaluation");return{outcome:A(o.outcome,["started","existing"],"an Evaluation outcome"),offerCode:v(n.offerCode,"an Evaluation offer"),expiresAt:v(n.expiresAt,"an Evaluation expiry")}}function a(t){var r,o;if(!(t instanceof e.ConsoleApiError)||"evaluation.start_denied"!==t.code)return;const n=null===(r=t.body)||void 0===r?void 0:r.reason,s=null===(o=t.body)||void 0===o?void 0:o.nextEligibleAt;return{message:t.message,..."string"==typeof n?{reason:n}:{},..."string"==typeof s?{nextEligibleAt:s}:{}}}async function i(e,r){return(0,t.parseProjectLinkManifest)(await e.request(`${f(r)}/link`,{method:"POST"}))}async function c(e,t){const r=g(await e.request(`${f(t)}/registry-access`),"the Registry access configuration");return{registryUrl:v(r.registryUrl,"a Registry URL"),scope:v(r.scope,"a Registry scope")}}async function u(e,t){const r=await e.request(`${f(t)}/registry-access-tokens`);if(!Array.isArray(r))throw x("the Registry Access Token list");return r.map(e=>p(e))}async function l(e,t,r){return m(await e.request(`${f(t)}/registry-access-tokens`,{method:"POST",body:{name:r}}))}async function d(e,t,r){return m(await e.request(`${f(t)}/registry-access-tokens/${encodeURIComponent(r)}/rotate`,{method:"POST"}))}async function y(e){const t=g(await e.request("/v1/me"),"the Console viewer");if(!t.customerAccount)return;const r=g(t.customerAccount,"the Customer Account");return{id:v(r.id,"a Customer Account id"),name:v(r.name,"a Customer Account name")}}function f(e){return`/v1/projects/${encodeURIComponent(e)}`}function p(e){const t=g(e,"a Registry Access Token");return{id:v(t.id,"a Registry Access Token id"),name:v(t.name,"a Registry Access Token name"),state:A(t.state,["active","expired","revoked"],"a token state")}}function m(e){const t=g(e,"the issued Registry Access Token");return{...p(e),token:v(t.token,"a Registry Access Token secret")}}function g(e,t){if(!e||"object"!=typeof e||Array.isArray(e))throw x(t);return e}function v(e,t){if("string"!=typeof e||0===e.length)throw x(t);return e}function A(e,t,r){if("string"!=typeof e||!t.includes(e))throw x(r);return e}function E(e,t,r){return null==e?null:A(e,t,r)}function x(e){return new Error(`Vendure Console returned ${e} in an unexpected shape.`)}exports.PROJECT_EDITIONS=["starter","growth","scale","enterprise"],exports.EVALUATION_OFFER_CODES=["starter","growth","scale"];
|