@sixtyninecommerce/admin 0.1.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 ADDED
@@ -0,0 +1,147 @@
1
+ # @sixtyninecommerce/admin
2
+
3
+ The back-office SDK for sixtynine-commerce. Every capability of the platform is a method here —
4
+ no raw GraphQL, no REST paths, no header names.
5
+
6
+ Built on [`@sixtyninecommerce/core`](https://www.npmjs.com/package/@sixtyninecommerce/core), which
7
+ gives it the tenant context, the error hierarchy, cursor pagination, idempotency keys and retries.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @sixtyninecommerce/admin
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { createAdminClient } from '@sixtyninecommerce/admin';
19
+
20
+ const admin = createAdminClient({
21
+ urls: {
22
+ vendureUrl: 'https://engine.sixtyninecommerce.com',
23
+ cmsUrl: 'https://cms.sixtyninecommerce.com',
24
+ pluginsUrl: 'https://plugins.sixtyninecommerce.com',
25
+ },
26
+ auth: { token: process.env.SIXTYNINE_API_TOKEN! },
27
+ tenant: { scope: { kind: 'channel', code: 'velvet-thread' } },
28
+ });
29
+
30
+ for await (const form of await admin.content.forms.list()) {
31
+ console.log(form.slug);
32
+ }
33
+ ```
34
+
35
+ ### Credentials
36
+
37
+ | Principal | How | Where |
38
+ |---|---|---|
39
+ | API token | `auth: { token: 's69t_…' }` | server only — constructing one in a browser throws `CREDENTIAL_IN_BROWSER` |
40
+ | Admin session | `auth: { session: { token?, onToken } }` | server or browser |
41
+
42
+ A session client can start empty and sign in:
43
+
44
+ ```ts
45
+ const session: { token?: string } = {};
46
+ const admin = createAdminClient({ urls, auth: { session } });
47
+ const me = await admin.auth.login('user@example.com', 'secret');
48
+ const tenant = admin.withTenant({ scope: { kind: 'channel', code: me.channels[0]!.code } });
49
+ ```
50
+
51
+ **The session object you pass IS where the bearer is stored.** Vendure returns a fresh one on a
52
+ response header and the SDK writes it back into `session.token`, so it must be writable — a plain
53
+ property, or a getter with a setter over your own store. A read-only one is refused at construction
54
+ with `SESSION_NOT_WRITABLE` rather than dropping every rotation silently. `onToken` is a
55
+ notification for persisting it across processes, and it fires with `''` when `logout()` ends the
56
+ session so a stored copy can be invalidated.
57
+
58
+ **In a server, give each request its own session object** — one shared object is one shared
59
+ credential:
60
+
61
+ ```ts
62
+ const admin = createAdminClient({
63
+ urls,
64
+ auth: {
65
+ session: {
66
+ get token() { return requestContext.token; },
67
+ set token(next: string | undefined) { requestContext.token = next; },
68
+ },
69
+ },
70
+ });
71
+ ```
72
+
73
+ A bearer is not proof of a session: Vendure mints an *anonymous* one for an unauthenticated read.
74
+ Treat "signed in" as "`auth.me()` resolved", never as "a token is stored".
75
+
76
+ ### What an API token cannot do
77
+
78
+ `apiTokens.*` needs a human session. The engine strips `ManageApiToken` from every token it
79
+ issues, so a token can never manage tokens — including its own — and `platform.*` refuses a token
80
+ in the SDK before the request leaves the process. Mint and revoke credentials from a signed-in
81
+ session:
82
+
83
+ ```ts
84
+ const session: { token?: string } = {};
85
+ const human = createAdminClient({ urls, auth: { session } });
86
+ await human.auth.login('ops@example.com', process.env.OPS_PASSWORD!);
87
+ const scoped = human.withTenant({ scope: { kind: 'channel', code: 'velvet-thread' } });
88
+
89
+ const created = await scoped.apiTokens.create({
90
+ name: 'ERP sync',
91
+ permissions: ['ReadOrder'],
92
+ expiresAt: '2027-01-01T00:00:00Z',
93
+ });
94
+ console.log(created.token); // shown exactly once
95
+ ```
96
+
97
+ ### Knowing what credential you hold
98
+
99
+ ```ts
100
+ const who = await admin.auth.whoami();
101
+ who.kind; // 'api_token' | 'user'
102
+ who.token?.expiresAt; // null means it never expires
103
+ who.permissions; // effective on the ACTIVE channel, not the union
104
+ who.entitlements; // connected plugin keys for that channel
105
+ ```
106
+
107
+ For a machine credential this is the only way to learn any of it without being refused. It needs a
108
+ tenant — permissions and entitlements have no meaning without a channel — and it is admin-plane
109
+ only: a storefront principal is refused with `PermissionDeniedError`.
110
+
111
+ ### Tenancy
112
+
113
+ A client with no `tenant.scope` may only call `auth.*` and `channels.byCode()`; every other module
114
+ throws `TENANT_REQUIRED`. An administrator with several channels must say which one they mean — the
115
+ server never guesses. `channels.byCode()` is the exception on purpose: it names the channel in its
116
+ own argument, and it is how you look up the code you are about to scope to. `withTenant()` derives
117
+ a client on another tenant, sharing the principal and the channel cache.
118
+
119
+ ### Writes
120
+
121
+ Every write takes `{ idempotencyKey?, tenant?, signal? }`. The key is sent as `Idempotency-Key` on a
122
+ REST write and dropped on a GraphQL one — the Admin API has no header for it, so an `apiTokens.*`
123
+ write is never deduplicated.
124
+
125
+ | Write | Deduplicated? |
126
+ |---|---|
127
+ | `content.forms.upsertBySlug` | yes — the CMS replays the first response for the same key |
128
+ | `seo.redirects.create` | yes — the plugins service does the same |
129
+ | everything else | no; the key is recorded on the wire and acts on nothing |
130
+
131
+ Where it is honoured, the same key with a **different** body is refused with 409. This SDK never
132
+ retries a write on its own, so a repeat is always one you sent: use a fresh key per intended write
133
+ and reuse it only when retrying that exact write.
134
+
135
+ ### Errors
136
+
137
+ One hierarchy, all re-exported from this package so you need no direct dependency on
138
+ `@sixtyninecommerce/core`: `ValidationError`, `AuthenticationError`, `PermissionDeniedError`,
139
+ `NotFoundError`, `ConflictError`, `BusinessRuleError`, `RateLimitedError`, `UpstreamError`,
140
+ `TransportError`, and `AdminProjectionError` — this package's own, thrown by `content.forms` when
141
+ the CMS answers a public route with its storefront projection because the credential was not
142
+ accepted for the channel (`ADMIN_PROJECTION_UNAVAILABLE`). All extend `PlatformError`. Branch on
143
+ `error.code`.
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,29 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { ApiToken, Call, CreateApiTokenInput, ListOptions, UpdateApiTokenInput } from '../types.js';
3
+ export declare const API_TOKEN_FIELDS = "\n id\n name\n tokenPrefix\n permissions\n channelId\n expiresAt\n lastUsedAt\n revokedAt\n createdAt\n";
4
+ export declare const LIST_QUERY = "\n query ApiTokens($options: ApiTokenListOptions) {\n apiTokens(options: $options) {\n totalItems\n items { \n id\n name\n tokenPrefix\n permissions\n channelId\n expiresAt\n lastUsedAt\n revokedAt\n createdAt\n }\n }\n }\n";
5
+ export declare const CREATE_MUTATION = "\n mutation CreateApiToken($input: CreateApiTokenInput!) {\n createApiToken(input: $input) {\n token\n apiToken { \n id\n name\n tokenPrefix\n permissions\n channelId\n expiresAt\n lastUsedAt\n revokedAt\n createdAt\n }\n }\n }\n";
6
+ export declare const UPDATE_MUTATION = "\n mutation UpdateApiToken($input: UpdateApiTokenInput!) {\n updateApiToken(input: $input) { \n id\n name\n tokenPrefix\n permissions\n channelId\n expiresAt\n lastUsedAt\n revokedAt\n createdAt\n }\n }\n";
7
+ export declare const REVOKE_MUTATION = "\n mutation RevokeApiToken($id: ID!) {\n revokeApiToken(id: $id) { \n id\n name\n tokenPrefix\n permissions\n channelId\n expiresAt\n lastUsedAt\n revokedAt\n createdAt\n }\n }\n";
8
+ export declare const DELETE_MUTATION = "\n mutation DeleteApiTokens($ids: [ID!]!) {\n deleteApiTokens(ids: $ids) {\n result\n message\n }\n }\n";
9
+ /** The raw token is shown exactly once, at creation. */
10
+ export interface CreatedApiToken {
11
+ token: string;
12
+ apiToken: ApiToken;
13
+ }
14
+ /**
15
+ * Managing API tokens needs a HUMAN SESSION. The engine strips
16
+ * `ManageApiToken` from every grant set it issues (`NEVER_GRANTABLE`), so a
17
+ * token can never manage tokens — including its own — and every method here
18
+ * answers `PermissionDeniedError FORBIDDEN` for a machine credential. That is
19
+ * the engine's rule, not the SDK's, so it is documented rather than guarded:
20
+ * the refusal arrives from the API that owns it.
21
+ */
22
+ export interface ApiTokensModule {
23
+ list(options?: ListOptions): Promise<Page<ApiToken>>;
24
+ create(input: CreateApiTokenInput, options?: Call): Promise<CreatedApiToken>;
25
+ update(id: string, input: UpdateApiTokenInput, options?: Call): Promise<ApiToken>;
26
+ revoke(id: string, options?: Call): Promise<ApiToken>;
27
+ delete(ids: string[], options?: Call): Promise<void>;
28
+ }
29
+ export declare function createApiTokensModule(transport: Transport): ApiTokensModule;
@@ -0,0 +1,90 @@
1
+ import { ConflictError, pageFromVendureList, readOffsetCursor, } from '@sixtyninecommerce/core';
2
+ export const API_TOKEN_FIELDS = /* GraphQL */ `
3
+ id
4
+ name
5
+ tokenPrefix
6
+ permissions
7
+ channelId
8
+ expiresAt
9
+ lastUsedAt
10
+ revokedAt
11
+ createdAt
12
+ `;
13
+ export const LIST_QUERY = /* GraphQL */ `
14
+ query ApiTokens($options: ApiTokenListOptions) {
15
+ apiTokens(options: $options) {
16
+ totalItems
17
+ items { ${API_TOKEN_FIELDS} }
18
+ }
19
+ }
20
+ `;
21
+ export const CREATE_MUTATION = /* GraphQL */ `
22
+ mutation CreateApiToken($input: CreateApiTokenInput!) {
23
+ createApiToken(input: $input) {
24
+ token
25
+ apiToken { ${API_TOKEN_FIELDS} }
26
+ }
27
+ }
28
+ `;
29
+ export const UPDATE_MUTATION = /* GraphQL */ `
30
+ mutation UpdateApiToken($input: UpdateApiTokenInput!) {
31
+ updateApiToken(input: $input) { ${API_TOKEN_FIELDS} }
32
+ }
33
+ `;
34
+ export const REVOKE_MUTATION = /* GraphQL */ `
35
+ mutation RevokeApiToken($id: ID!) {
36
+ revokeApiToken(id: $id) { ${API_TOKEN_FIELDS} }
37
+ }
38
+ `;
39
+ export const DELETE_MUTATION = /* GraphQL */ `
40
+ mutation DeleteApiTokens($ids: [ID!]!) {
41
+ deleteApiTokens(ids: $ids) {
42
+ result
43
+ message
44
+ }
45
+ }
46
+ `;
47
+ const DEFAULT_PAGE_SIZE = 25;
48
+ export function createApiTokensModule(transport) {
49
+ async function listPage(skip, take, options) {
50
+ const data = await transport.graphql({ api: 'admin', query: LIST_QUERY, variables: { options: { skip, take } } }, options);
51
+ return pageFromVendureList(data.apiTokens, skip, take, (cursor) => listPage(readOffsetCursor(cursor), take, options));
52
+ }
53
+ return {
54
+ list(options) {
55
+ // The Admin API is offset-paged; the offset is hidden inside the cursor so
56
+ // a consumer only ever sees `Page<T>` (spec §9.3).
57
+ const take = options?.limit ?? DEFAULT_PAGE_SIZE;
58
+ const skip = options?.after === undefined ? 0 : readOffsetCursor(options.after);
59
+ return listPage(skip, take, options);
60
+ },
61
+ async create(input, options) {
62
+ const data = await transport.graphql({ api: 'admin', query: CREATE_MUTATION, variables: { input } }, options);
63
+ return data.createApiToken;
64
+ },
65
+ async update(id, input, options) {
66
+ const data = await transport.graphql({ api: 'admin', query: UPDATE_MUTATION, variables: { input: { id, ...input } } }, options);
67
+ return data.updateApiToken;
68
+ },
69
+ async revoke(id, options) {
70
+ const data = await transport.graphql({ api: 'admin', query: REVOKE_MUTATION, variables: { id } }, options);
71
+ return data.revokeApiToken;
72
+ },
73
+ async delete(ids, options) {
74
+ const data = await transport.graphql({ api: 'admin', query: DELETE_MUTATION, variables: { ids } }, options);
75
+ // `deleteApiTokens` reports per-id outcomes at HTTP 200 with no error, so
76
+ // a caller awaiting a void promise would read a refusal as success.
77
+ const refused = data.deleteApiTokens.filter((entry) => entry.result !== 'DELETED');
78
+ if (refused.length > 0) {
79
+ throw new ConflictError({
80
+ code: 'API_TOKEN_NOT_DELETED',
81
+ message: `${String(refused.length)} of ${String(ids.length)} tokens were not deleted: ${refused
82
+ .map((entry) => entry.message ?? 'no reason given')
83
+ .join('; ')}`,
84
+ status: 409,
85
+ service: 'vendure',
86
+ });
87
+ }
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,61 @@
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, Me, WhoAmI } from '../types.js';
3
+ export interface AuthModule {
4
+ /** Establishes a session. The bearer arrives on a response header, not here. */
5
+ login(username: string, password: string, options?: Call): Promise<Me>;
6
+ /**
7
+ * Ends the session server-side AND clears the bearer, in the caller's own
8
+ * session object. `onToken` fires with `''` so a persisted copy — a cookie, a
9
+ * keychain entry, `localStorage` — can be invalidated in the same step.
10
+ *
11
+ * Idempotent: with no bearer in hand there is nothing to end, and asking
12
+ * anyway would make Vendure mint an anonymous session.
13
+ */
14
+ logout(options?: Call): Promise<void>;
15
+ me(options?: Call): Promise<Me>;
16
+ /**
17
+ * Describe the credential in hand: its kind, its channels, and — for the
18
+ * ACTIVE channel — its effective permissions and the plugin entitlements its
19
+ * tenant has. For a machine credential this is the only way to learn any of
20
+ * that without being refused; `token.expiresAt` is what an integration checks
21
+ * against now.
22
+ *
23
+ * Unlike the rest of `auth`, it needs a tenant: permissions and entitlements
24
+ * have no meaning without a channel, and the platform refuses to guess one.
25
+ * It is also an admin-plane endpoint — a storefront principal is refused with
26
+ * `PermissionDeniedError`.
27
+ */
28
+ whoami(options?: Call): Promise<WhoAmI>;
29
+ }
30
+ /** The session bearer's store, when the client has one (a token client does not). */
31
+ export interface SessionControl {
32
+ /** Is there a bearer to send at all? */
33
+ present(): boolean;
34
+ /** The bearer in force right now — what the next call will send. */
35
+ current(): string;
36
+ /** How many rotations this client has STORED, to tell one call's apart. */
37
+ rotations(): number;
38
+ /**
39
+ * Forget it. `sent` makes it conditional: clear only while the slot still
40
+ * holds that bearer, so a late response proving one credential dead cannot
41
+ * sign out the session that replaced it.
42
+ */
43
+ end(sent?: string): void;
44
+ }
45
+ /**
46
+ * `session` is present only for a session principal. It exists for two reasons.
47
+ *
48
+ * `logout()` has to clear the bearer — Vendure signals sign-out with an EMPTY
49
+ * `vendure-auth-token` header, which the transport correctly refuses to treat
50
+ * as a rotation, so nothing else ever would.
51
+ *
52
+ * And `me()`/`logout()` must not go out at all without one. `me` is
53
+ * `@Allow(Authenticated, Owner)`, and for an Owner-allowed operation Vendure
54
+ * MINTS AN ANONYMOUS SESSION for an unauthenticated request and returns its
55
+ * token on `vendure-auth-token`. The SDK would store that as if it were a
56
+ * credential, so a dashboard that signed out and re-checked `me()` would have a
57
+ * bearer again — and an app gating its shell on "is a token stored?" would show
58
+ * itself as signed in. Refusing before the wire is also the honest answer: with
59
+ * no bearer there is no session to report on.
60
+ */
61
+ export declare function createAuthModule(transport: Transport, session?: SessionControl): AuthModule;
@@ -0,0 +1,162 @@
1
+ import { AuthenticationError, errorFromErrorResult, isErrorResult, PermissionDeniedError, UNSCOPED_TENANT, } from '@sixtyninecommerce/core';
2
+ import { LOGIN_MUTATION, LOGOUT_MUTATION, ME_QUERY } from './queries.js';
3
+ /**
4
+ * `me` is nullable on the Admin API: an unauthenticated request is answered
5
+ * with `null`, not with an error. Returning that as a `Me` would hand the
6
+ * caller `undefined` under a `string` type, so it is turned into the
7
+ * authentication failure it actually is.
8
+ */
9
+ /**
10
+ * A bearer the Admin API does not accept makes `me` answer `FORBIDDEN`, which
11
+ * reads as PermissionDeniedError — "you may not do this" — when the truth is
12
+ * "your session is over". That sends a dashboard to an error page instead of to
13
+ * its sign-in screen, so it is narrowed for `me` ONLY; everywhere else
14
+ * FORBIDDEN keeps its meaning.
15
+ *
16
+ * Verified against the installed engine rather than assumed: `me` is
17
+ * `@Allow(Permission.Authenticated, Permission.Owner)`, and because `Owner` is
18
+ * in that list the guard never refuses it — every FORBIDDEN comes from
19
+ * `BaseAuthResolver.me`, which throws when there is no active user AND when an
20
+ * authenticated user has no Administrator row. The second is a real
21
+ * authorization refusal, but it is unreachable through this SDK: `login()`
22
+ * refuses a non-administrator on the Admin API in the first place. So the
23
+ * narrowing is safe for every session this client can hold, and `cause` keeps
24
+ * the original for the case that is not.
25
+ */
26
+ function asAuthFailure(error) {
27
+ if (!(error instanceof PermissionDeniedError))
28
+ return error;
29
+ return new AuthenticationError({
30
+ code: 'NOT_AUTHENTICATED',
31
+ message: 'No active session. Call auth.login() first, or supply an API token.',
32
+ status: 401,
33
+ service: 'vendure',
34
+ ...(error.traceId ? { traceId: error.traceId } : {}),
35
+ cause: error,
36
+ });
37
+ }
38
+ function readMe(data) {
39
+ const me = data.me;
40
+ if (!me) {
41
+ // Built directly rather than run through `errorFromErrorResult`: Vendure
42
+ // sent no ErrorResult, it sent `null`. Synthesising one would land on the
43
+ // default branch and classify a missing session as a business rule.
44
+ throw new AuthenticationError({
45
+ code: 'NOT_AUTHENTICATED',
46
+ message: 'No active session. Call auth.login() first, or supply an API token.',
47
+ status: 401,
48
+ service: 'vendure',
49
+ });
50
+ }
51
+ return {
52
+ id: me.id,
53
+ identifier: me.identifier,
54
+ channels: me.channels,
55
+ administrator: data.activeAdministrator,
56
+ };
57
+ }
58
+ /**
59
+ * `session` is present only for a session principal. It exists for two reasons.
60
+ *
61
+ * `logout()` has to clear the bearer — Vendure signals sign-out with an EMPTY
62
+ * `vendure-auth-token` header, which the transport correctly refuses to treat
63
+ * as a rotation, so nothing else ever would.
64
+ *
65
+ * And `me()`/`logout()` must not go out at all without one. `me` is
66
+ * `@Allow(Authenticated, Owner)`, and for an Owner-allowed operation Vendure
67
+ * MINTS AN ANONYMOUS SESSION for an unauthenticated request and returns its
68
+ * token on `vendure-auth-token`. The SDK would store that as if it were a
69
+ * credential, so a dashboard that signed out and re-checked `me()` would have a
70
+ * bearer again — and an app gating its shell on "is a token stored?" would show
71
+ * itself as signed in. Refusing before the wire is also the honest answer: with
72
+ * no bearer there is no session to report on.
73
+ */
74
+ export function createAuthModule(transport, session) {
75
+ const notAuthenticated = () => new AuthenticationError({
76
+ code: 'NOT_AUTHENTICATED',
77
+ message: 'No active session. Call auth.login() first, or supply an API token.',
78
+ status: 401,
79
+ service: 'vendure',
80
+ });
81
+ /**
82
+ * Auth documents go out UNSCOPED, even on a tenant-scoped client: a session
83
+ * belongs to the user, not to a tenant, and `login` happens before the caller
84
+ * knows which channels it may address. Sending a channel token there would
85
+ * also make a client that has no tenant yet unable to sign in at all.
86
+ */
87
+ const unscoped = (options) => ({
88
+ ...options,
89
+ tenant: UNSCOPED_TENANT,
90
+ allowUnscoped: true,
91
+ });
92
+ async function me(options) {
93
+ if (session && !session.present())
94
+ throw notAuthenticated();
95
+ const sent = session?.current();
96
+ try {
97
+ const data = await transport.graphql({ api: 'admin', query: ME_QUERY }, unscoped(options));
98
+ return readMe(data);
99
+ }
100
+ catch (error) {
101
+ const failure = asAuthFailure(error);
102
+ // A bearer the engine refused is dead, and `me` is Owner-allowed — so
103
+ // Vendure answered this very request by MINTING an anonymous session and
104
+ // returning its token, which the client has just stored as if it were a
105
+ // credential. Drop it, or `present()` reports a session the user never
106
+ // obtained and an app gating on "is a token stored?" stays signed in.
107
+ if (failure instanceof AuthenticationError)
108
+ session?.end(sent);
109
+ throw failure;
110
+ }
111
+ }
112
+ return {
113
+ async login(username, password, options) {
114
+ const rotationsBefore = session?.rotations();
115
+ const data = await transport.graphql({ api: 'admin', query: LOGIN_MUTATION, variables: { username, password } }, unscoped(options));
116
+ // Vendure answers a bad password with HTTP 200 and an ErrorResult member.
117
+ if (isErrorResult(data.login))
118
+ throw errorFromErrorResult(data.login);
119
+ // The session bearer arrives on the `vendure-auth-token` response header.
120
+ // If none did, the credentials were right and the client still has no way
121
+ // to authenticate anything — say that, rather than let the `me()` below
122
+ // report it as "no active session" after a successful sign-in.
123
+ //
124
+ // Asked as "did THIS sign-in store a bearer", never as "is some bearer
125
+ // present": a client that already held one — an account switch, or a
126
+ // token restored from a keychain — would otherwise skip the check, keep
127
+ // the OLD bearer, and resolve `login()` as the administrator who was
128
+ // already signed in. Vendure does not invalidate their session, so every
129
+ // later call would act as them, audit trail included.
130
+ if (session && session.rotations() === rotationsBefore) {
131
+ throw new AuthenticationError({
132
+ code: 'NO_SESSION_BEARER',
133
+ message: 'The engine accepted the credentials but returned no vendure-auth-token header, so this client has no bearer to send. The Admin API must be configured with the `bearer` token method.',
134
+ status: 401,
135
+ service: 'vendure',
136
+ });
137
+ }
138
+ return me(options);
139
+ },
140
+ async logout(options) {
141
+ // Idempotent, and it must not go out unauthenticated: Vendure would mint
142
+ // an anonymous session and hand back its token, arming the client again.
143
+ if (session && !session.present())
144
+ return;
145
+ await transport.graphql({ api: 'admin', query: LOGOUT_MUTATION }, unscoped(options));
146
+ // Only after the server confirms: clearing on a failed logout would tell
147
+ // the caller they are signed out while the session is still live.
148
+ session?.end();
149
+ },
150
+ // `async` so the guard REJECTS rather than throwing synchronously: every
151
+ // other method on this client returns a rejected promise, and a caller who
152
+ // only attached `.catch()` would otherwise get an uncatchable throw.
153
+ async whoami(options) {
154
+ if (session && !session.present())
155
+ throw notAuthenticated();
156
+ // Scoped on purpose: `x-channel-id` is what selects the active channel,
157
+ // and the service refuses a multi-channel principal that names none.
158
+ return transport.rest('plugins', { method: 'GET', path: '/me' }, options);
159
+ },
160
+ me,
161
+ };
162
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The Admin API documents this SDK's auth uses.
3
+ *
4
+ * `LOGIN` is sent WITHOUT a channel token: a session belongs to the user, not
5
+ * to a tenant, and the channels the user may address come back from `ME`.
6
+ * Vendure returns the session bearer on the `vendure-auth-token` RESPONSE
7
+ * header, which the transport hands to `Principal.onToken`.
8
+ */
9
+ export declare const LOGIN_MUTATION = "\n mutation Login($username: String!, $password: String!) {\n login(username: $username, password: $password) {\n __typename\n ... on CurrentUser {\n id\n }\n ... on ErrorResult {\n errorCode\n message\n }\n }\n }\n";
10
+ export declare const ME_QUERY = "\n query Me {\n me {\n id\n identifier\n channels {\n id\n code\n token\n permissions\n }\n }\n activeAdministrator {\n id\n firstName\n lastName\n emailAddress\n }\n }\n";
11
+ export declare const LOGOUT_MUTATION = "\n mutation Logout {\n logout {\n success\n }\n }\n";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The Admin API documents this SDK's auth uses.
3
+ *
4
+ * `LOGIN` is sent WITHOUT a channel token: a session belongs to the user, not
5
+ * to a tenant, and the channels the user may address come back from `ME`.
6
+ * Vendure returns the session bearer on the `vendure-auth-token` RESPONSE
7
+ * header, which the transport hands to `Principal.onToken`.
8
+ */
9
+ export const LOGIN_MUTATION = /* GraphQL */ `
10
+ mutation Login($username: String!, $password: String!) {
11
+ login(username: $username, password: $password) {
12
+ __typename
13
+ ... on CurrentUser {
14
+ id
15
+ }
16
+ ... on ErrorResult {
17
+ errorCode
18
+ message
19
+ }
20
+ }
21
+ }
22
+ `;
23
+ export const ME_QUERY = /* GraphQL */ `
24
+ query Me {
25
+ me {
26
+ id
27
+ identifier
28
+ channels {
29
+ id
30
+ code
31
+ token
32
+ permissions
33
+ }
34
+ }
35
+ activeAdministrator {
36
+ id
37
+ firstName
38
+ lastName
39
+ emailAddress
40
+ }
41
+ }
42
+ `;
43
+ export const LOGOUT_MUTATION = /* GraphQL */ `
44
+ mutation Logout {
45
+ logout {
46
+ success
47
+ }
48
+ }
49
+ `;
@@ -0,0 +1,83 @@
1
+ import { type DeprecationNotice, type RateLimitInfo, type RetryPolicy, type TenantContext, type TenantScope } from '@sixtyninecommerce/core';
2
+ import { type ApiTokensModule } from './api-tokens/index.js';
3
+ import { type AuthModule } from './auth/index.js';
4
+ import { type FormsModule } from './content/forms.js';
5
+ import { type RedirectsModule } from './seo/redirects.js';
6
+ import { type ChannelsModule } from './tenant/channels.js';
7
+ export interface AdminClientConfig {
8
+ urls: {
9
+ vendureUrl: string;
10
+ cmsUrl: string;
11
+ pluginsUrl: string;
12
+ };
13
+ /**
14
+ * A machine credential (`token`) or a human session.
15
+ *
16
+ * A session may start empty: the client holds the bearer `login()` obtains,
17
+ * so nothing has to be stored to use it. It never writes back into this
18
+ * object — `onToken` is the only way a caller learns the bearer, and it is
19
+ * what to persist across processes.
20
+ *
21
+ * `token` is read LIVE on every call, so a getter over the caller's own
22
+ * store works: whenever that value changes it becomes the bearer, which is
23
+ * how a per-request credential, a token restored after construction, and a
24
+ * sign-out that clears it all take effect.
25
+ */
26
+ auth: {
27
+ token: string;
28
+ } | {
29
+ session: SessionConfig;
30
+ };
31
+ tenant?: Partial<TenantContext> & {
32
+ scope?: TenantScope;
33
+ };
34
+ fetch?: typeof globalThis.fetch;
35
+ retry?: Partial<RetryPolicy>;
36
+ timeoutMs?: number;
37
+ onDeprecation?: (notice: DeprecationNotice) => void;
38
+ }
39
+ export interface AdminClient {
40
+ auth: AuthModule;
41
+ apiTokens: ApiTokensModule;
42
+ channels: ChannelsModule;
43
+ content: {
44
+ forms: FormsModule;
45
+ };
46
+ seo: {
47
+ redirects: RedirectsModule;
48
+ };
49
+ /** Human SuperAdmin surface. Empty in phase 0. */
50
+ platform: Record<string, never>;
51
+ /** A derived client on another tenant, sharing this one's principal. */
52
+ withTenant(tenant: Partial<TenantContext> & {
53
+ scope?: TenantScope;
54
+ }): AdminClient;
55
+ readonly rateLimit: RateLimitInfo | null;
56
+ }
57
+ export interface SessionConfig {
58
+ /**
59
+ * The session bearer, and where this client STORES it. Vendure returns a
60
+ * fresh one on a response header, and the SDK writes it back here — so this
61
+ * object, not a hidden slot inside the client, is the single source of truth.
62
+ * It must be writable (a plain property, or a getter WITH a setter over your
63
+ * own store); `createAdminClient` refuses a read-only one rather than
64
+ * silently dropping every rotation.
65
+ *
66
+ * **In a server, give each request its own session object** (or a
67
+ * getter/setter pair over your request context). One shared object is one
68
+ * shared credential: the bearer the last sign-in produced is the bearer the
69
+ * next request sends.
70
+ */
71
+ token?: string;
72
+ /**
73
+ * Called with every bearer this client stores, for persisting it across
74
+ * processes — and with `''` when `logout()` ends the session, so a stored
75
+ * copy can be invalidated.
76
+ *
77
+ * A bearer is not proof of a session: Vendure mints an ANONYMOUS session for
78
+ * an unauthenticated read. Treat "signed in" as "`auth.me()` resolved", never
79
+ * as "a token is stored".
80
+ */
81
+ onToken?: (token: string) => void;
82
+ }
83
+ export declare function createAdminClient(config: AdminClientConfig): AdminClient;