Skip to content

Commit 37e3078

Browse files
committed
Move new top-level auth mode types out of js-client
1 parent df97dea commit 37e3078

4 files changed

Lines changed: 41 additions & 160 deletions

File tree

packages/api-client-core/spec/GadgetConnection-suite.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -71,38 +71,6 @@ export const GadgetConnectionSharedSuite = (queryExtra = "") => {
7171
});
7272

7373
describe("authorization", () => {
74-
describe("when declaring authentication modes at the top level and under the `authenticationMode` key at the same time", () => {
75-
it("should throw an error if same auth modes are declared", () => {
76-
expect(
77-
() =>
78-
new GadgetConnection({
79-
endpoint: "https://someapp.gadget.app/api/graphql",
80-
authenticationMode: { browserSession: true },
81-
browserSession: true,
82-
} as any)
83-
).toThrowErrorMatchingInlineSnapshot(
84-
`"Declaring authentication modes at the top level and under the \`authenticationMode\` key at the same time is not allowed."`
85-
);
86-
});
87-
88-
it("should throw an error if different auth modes are declared", () => {
89-
expect(
90-
() =>
91-
new GadgetConnection({
92-
endpoint: "https://someapp.gadget.app/api/graphql",
93-
authenticationMode: {
94-
browserSession: {
95-
shopId: "1234",
96-
},
97-
},
98-
anonymous: true,
99-
} as any)
100-
).toThrowErrorMatchingInlineSnapshot(
101-
`"Declaring authentication modes at the top level and under the \`authenticationMode\` key at the same time is not allowed."`
102-
);
103-
});
104-
});
105-
10674
it("should allow connecting with anonymous authentication", async () => {
10775
nock("https://someapp.gadget.app")
10876
.post("/api/graphql?operation=meta", { query: `{\n meta {\n appName\n${queryExtra} }\n}`, variables: {} })
@@ -806,8 +774,10 @@ export const GadgetConnectionSharedSuite = (queryExtra = "") => {
806774

807775
const connection = new GadgetConnection({
808776
endpoint: "https://someapp.gadget.app/api/graphql",
809-
browserSession: {
810-
shopId: "1234",
777+
authenticationMode: {
778+
browserSession: {
779+
shopId: "1234",
780+
},
811781
},
812782
});
813783

packages/api-client-core/src/ClientOptions.ts

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import type { Exchange } from "@urql/core";
22
import type { GadgetSubscriptionClientOptions } from "./GadgetConnection.js";
33

4-
type BaseClientOptions = {
4+
/** All the options for a Gadget client */
5+
export interface ClientOptions {
56
/**
67
* The HTTP GraphQL endpoint this connection should connect to
78
**/
89
endpoint?: string;
10+
/**
11+
* The authentication strategy for connecting to the upstream API
12+
**/
13+
authenticationMode?: AuthenticationModeOptions;
914
/**
1015
* The Websockets GraphQL endpoint this connection should connect to for transactional processing
1116
**/
@@ -40,24 +45,7 @@ type BaseClientOptions = {
4045
* A list of exchanges to merge into the default exchanges used by the client.
4146
*/
4247
exchanges?: Exchanges;
43-
};
44-
45-
/** All the options for a Gadget client */
46-
export type ClientOptions = BaseClientOptions &
47-
(
48-
| {
49-
/**
50-
* The authentication strategy for connecting to the upstream API.
51-
*
52-
* Note: you can only declare authentication modes at the top level, or under the `authenticationMode` key.
53-
* If you declare them at the top level and under the `authenticationMode` key at the same time, an error will be thrown.
54-
**/
55-
authenticationMode?: AuthenticationModeOptions;
56-
}
57-
| ({
58-
authenticationMode?: never;
59-
} & AuthenticationModeOptions)
60-
);
48+
}
6149

6250
/** Options to configure a specific browser-based authentication mode */
6351
export interface BrowserSessionAuthenticationModeOptions {
@@ -95,46 +83,31 @@ export enum BrowserSessionStorageType {
9583
Temporary = "temporary",
9684
}
9785

98-
/** Describes how to authenticate an instance of the client with the Gadget platform. */
86+
/** Describes how to authenticate an instance of the client with the Gadget platform */
9987
export interface AuthenticationModeOptions {
100-
/**
101-
* Use an API key to authenticate with Gadget.
102-
* It's not strictly required, but without this the client might be useless depending on the app's permissions.
103-
*/
88+
// Use an API key to authenticate with Gadget.
89+
// Not strictly required, but without this the client might be useless depending on the app's permissions.
10490
apiKey?: string;
10591

106-
/**
107-
* Use a web browser's `localStorage` or `sessionStorage` to persist authentication information.
108-
* This allows the browser to have a persistent identity as the user navigates around and logs in and out.
109-
*/
92+
// Use a web browser's `localStorage` or `sessionStorage` to persist authentication information.
93+
// This allows the browser to have a persistent identity as the user navigates around and logs in and out.
11094
browserSession?: boolean | BrowserSessionAuthenticationModeOptions;
11195

112-
/**
113-
* Use no authentication at all, and get access only to the data that the Unauthenticated backend role has access to.
114-
*/
96+
// Use no authentication at all, and get access only to the data that the Unauthenticated backend role has access to.
11597
anonymous?: true;
11698

117-
/**
118-
* @deprecated Use internal instead.
119-
*/
99+
// @deprecated Use internal instead
120100
internalAuthToken?: string;
121101

122-
/**
123-
* Use an internal platform auth token for authentication
124-
* This is used to communicate within Gadget itself and shouldn't be used to connect to Gadget from other systems.
125-
* @private
126-
*/
102+
// @private Use an internal platform auth token for authentication
103+
// This is used to communicate within Gadget itself and shouldn't be used to connect to Gadget from other systems
127104
internal?: {
128105
authToken: string;
129106
actAsSession?: boolean;
130107
getSessionId?: () => Promise<string | undefined>;
131108
};
132109

133-
/**
134-
* Use a passed custom function for managing authentication.
135-
* For some fancy integrations that the API client supports, like embedded Shopify apps, we use platform native features to authenticate with the Gadget backend.
136-
* @private
137-
*/
110+
// @private Use a passed custom function for managing authentication. For some fancy integrations that the API client supports, like embedded Shopify apps, we use platform native features to authenticate with the Gadget backend.
138111
custom?: {
139112
processFetch(input: RequestInfo | URL, init: RequestInit): Promise<void>;
140113
processTransactionConnectionParams(params: Record<string, any>): Promise<void>;

packages/api-client-core/src/GadgetConnection.ts

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import {
1919
GadgetTooManyRequestsError,
2020
GadgetUnexpectedCloseError,
2121
GadgetWebsocketConnectionTimeoutError,
22-
availableAuthenticationModes,
2322
isCloseEvent,
24-
maybeGetAuthenticationModeOptionsFromConnectionOptions,
2523
storageAvailable,
2624
} from "./support.js";
2725

@@ -48,8 +46,9 @@ export const $gadgetConnection = Symbol.for("gadget/connection");
4846
const sessionStorageKey = "token";
4947
const base64 = typeof btoa !== "undefined" ? btoa : (str: string) => Buffer.from(str).toString("base64");
5048

51-
type BaseGadgetConnectionOptions = {
49+
export interface GadgetConnectionOptions {
5250
endpoint: string;
51+
authenticationMode?: AuthenticationModeOptions;
5352
websocketsEndpoint?: string;
5453
subscriptionClientOptions?: GadgetSubscriptionClientOptions;
5554
websocketImplementation?: typeof globalThis.WebSocket;
@@ -60,17 +59,7 @@ type BaseGadgetConnectionOptions = {
6059
baseRouteURL?: string;
6160
exchanges?: Exchanges;
6261
createSubscriptionClient?: typeof createSubscriptionClient;
63-
};
64-
65-
export type GadgetConnectionOptions = BaseGadgetConnectionOptions &
66-
(
67-
| {
68-
authenticationMode?: AuthenticationModeOptions;
69-
}
70-
| ({
71-
authenticationMode?: never;
72-
} & AuthenticationModeOptions)
73-
);
62+
}
7463

7564
/**
7665
* Represents the current strategy for authenticating with the Gadget platform.
@@ -95,7 +84,6 @@ const objectForGlobals = typeof globalThis != "undefined" ? globalThis : typeof
9584
*/
9685
export class GadgetConnection {
9786
static version = "<prerelease>" as const;
98-
static availableAuthenticationModes = availableAuthenticationModes;
9987

10088
// Options used when generating new GraphQL clients for the base connection and for for transactions
10189
readonly endpoint: string;
@@ -121,15 +109,7 @@ export class GadgetConnection {
121109
private requestPolicy: RequestPolicy;
122110
createSubscriptionClient: typeof createSubscriptionClient;
123111

124-
/**
125-
* The authentication mode that came from the connection options in the constructor.
126-
* We have two methods of setting the authentication mode, so we're storing it separately to avoid having to manually determine which method was used.
127-
*/
128-
private authenticationModeOptions?: AuthenticationModeOptions;
129-
130112
constructor(readonly options: GadgetConnectionOptions) {
131-
this.authenticationModeOptions = maybeGetAuthenticationModeOptionsFromConnectionOptions(options);
132-
133113
if (!options.endpoint) throw new Error("Must provide an `endpoint` option for a GadgetConnection to connect to");
134114
this.endpoint = options.endpoint;
135115
if (options.fetchImplementation) {
@@ -161,7 +141,7 @@ export class GadgetConnection {
161141
};
162142
this.createSubscriptionClient = options.createSubscriptionClient ?? createSubscriptionClient;
163143

164-
this.setAuthenticationMode(this.authenticationModeOptions);
144+
this.setAuthenticationMode(options.authenticationMode);
165145

166146
this.baseClient = this.newBaseClient();
167147
}
@@ -196,7 +176,7 @@ export class GadgetConnection {
196176
} else if (options.custom) {
197177
this.authenticationMode = AuthenticationMode.Custom;
198178
}
199-
this.authenticationModeOptions = options;
179+
this.options.authenticationMode = options;
200180
}
201181

202182
this.authenticationMode ??= AuthenticationMode.Anonymous;
@@ -337,7 +317,7 @@ export class GadgetConnection {
337317
init.headers = { ...requestHeaders, ...init.headers };
338318

339319
if (this.authenticationMode == AuthenticationMode.Custom) {
340-
await this.authenticationModeOptions!.custom!.processFetch(input, init);
320+
await this.options.authenticationMode!.custom!.processFetch(input, init);
341321
}
342322
}
343323

@@ -491,24 +471,24 @@ export class GadgetConnection {
491471
// In the browser, we can't set arbitrary headers on the websocket request, so we don't use the same auth mechanism that we use for normal HTTP requests. Instead we use graphql-ws' connectionParams to send the auth information in the connection setup message to the server.
492472
const connectionParams: Record<string, any> = { environment: this.environment, auth: { type: this.authenticationMode } };
493473
if (this.authenticationMode == AuthenticationMode.APIKey) {
494-
connectionParams.auth.key = this.authenticationModeOptions!.apiKey!;
474+
connectionParams.auth.key = this.options.authenticationMode!.apiKey!;
495475
} else if (
496476
this.authenticationMode == AuthenticationMode.Internal ||
497477
this.authenticationMode == AuthenticationMode.InternalAuthToken
498478
) {
499479
const authToken =
500480
this.authenticationMode == AuthenticationMode.Internal
501-
? this.authenticationModeOptions!.internal!.authToken
502-
: this.authenticationModeOptions!.internalAuthToken!;
481+
? this.options.authenticationMode!.internal!.authToken
482+
: this.options.authenticationMode!.internalAuthToken!;
503483
connectionParams.auth.token = authToken;
504-
if (this.authenticationMode == AuthenticationMode.Internal && this.authenticationModeOptions!.internal!.actAsSession) {
484+
if (this.authenticationMode == AuthenticationMode.Internal && this.options.authenticationMode!.internal!.actAsSession) {
505485
connectionParams.auth.actAsInternalSession = true;
506-
connectionParams.auth.internalSessionId = await this.authenticationModeOptions!.internal!.getSessionId?.();
486+
connectionParams.auth.internalSessionId = await this.options.authenticationMode!.internal!.getSessionId?.();
507487
}
508488
} else if (this.authenticationMode == AuthenticationMode.BrowserSession) {
509489
connectionParams.auth.sessionToken = this.sessionTokenStore!.getItem(this.sessionStorageKey);
510490
} else if (this.authenticationMode == AuthenticationMode.Custom) {
511-
await this.authenticationModeOptions?.custom?.processTransactionConnectionParams(connectionParams);
491+
await this.options.authenticationMode?.custom?.processTransactionConnectionParams(connectionParams);
512492
}
513493
return connectionParams;
514494
},
@@ -519,11 +499,8 @@ export class GadgetConnection {
519499
connected: (socket, payload) => {
520500
// If we're using session token authorization, we don't use request headers to exchange the session token, we use graphql-ws' ConnectionAck payload to persist the token. When the subscription client first starts, the server will send us session token identifying this client, and we persist it to the session token store
521501
if (this.authenticationMode == AuthenticationMode.BrowserSession && payload?.sessionToken) {
522-
const browserSession = this.authenticationModeOptions?.browserSession;
523-
const initialToken =
524-
browserSession !== null && typeof browserSession === "object" && "initialToken" in browserSession
525-
? browserSession.initialToken
526-
: null;
502+
const browserSession = this.options.authenticationMode?.browserSession;
503+
const initialToken = browserSession !== null && typeof browserSession === "object" ? browserSession.initialToken : null;
527504
if (!initialToken) {
528505
this.sessionTokenStore!.setItem(this.sessionStorageKey, payload.sessionToken as string);
529506
}
@@ -556,28 +533,28 @@ export class GadgetConnection {
556533
if (this.authenticationMode == AuthenticationMode.Internal || this.authenticationMode == AuthenticationMode.InternalAuthToken) {
557534
const authToken =
558535
this.authenticationMode == AuthenticationMode.Internal
559-
? this.authenticationModeOptions!.internal!.authToken
560-
: this.authenticationModeOptions!.internalAuthToken!;
536+
? this.options.authenticationMode!.internal!.authToken
537+
: this.options.authenticationMode!.internalAuthToken!;
561538

562539
headers.authorization = "Basic " + base64("gadget-internal" + ":" + authToken);
563540

564-
if (this.authenticationMode == AuthenticationMode.Internal && this.authenticationModeOptions!.internal!.actAsSession) {
541+
if (this.authenticationMode == AuthenticationMode.Internal && this.options.authenticationMode!.internal!.actAsSession) {
565542
headers["x-gadget-act-as-internal-session"] = "true";
566543

567-
const sessionId = await this.authenticationModeOptions!.internal!.getSessionId?.();
544+
const sessionId = await this.options.authenticationMode!.internal!.getSessionId?.();
568545
if (sessionId) {
569546
headers["x-gadget-internal-session-id"] = sessionId;
570547
}
571548
}
572549
} else if (this.authenticationMode == AuthenticationMode.APIKey) {
573-
headers.authorization = `Bearer ${this.authenticationModeOptions?.apiKey}`;
550+
headers.authorization = `Bearer ${this.options.authenticationMode?.apiKey}`;
574551
} else if (this.authenticationMode == AuthenticationMode.BrowserSession) {
575552
const val = this.sessionTokenStore!.getItem(this.sessionStorageKey);
576553
if (val) {
577554
headers.authorization = `Session ${val}`;
578555
}
579556

580-
const browserSessionOptions = this.authenticationModeOptions!.browserSession!;
557+
const browserSessionOptions = this.options.authenticationMode!.browserSession!;
581558
const shopId = typeof browserSessionOptions === "boolean" ? undefined : browserSessionOptions.shopId;
582559
if (shopId) {
583560
headers["x-gadget-for-shop-id"] = shopId;

packages/api-client-core/src/support.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { OperationResult } from "@urql/core";
22
import { CombinedError } from "@urql/core";
33
import { Call, type FieldSelection as BuilderFieldSelection } from "tiny-graphql-query-compiler";
4-
import type { AuthenticationModeOptions, ClientOptions } from "./ClientOptions.js";
54
import { DataHydrator } from "./DataHydrator.js";
6-
import type { GadgetConnectionOptions } from "./GadgetConnection.js";
75
import type { ActionFunctionMetadata, AnyActionFunction } from "./GadgetFunctions.js";
86
import type { RecordShape } from "./GadgetRecord.js";
97
import { GadgetRecord } from "./GadgetRecord.js";
@@ -754,40 +752,3 @@ export const formatErrorMessages = (error: Error) => {
754752

755753
return result;
756754
};
757-
758-
export const availableAuthenticationModes = [
759-
"apiKey",
760-
"browserSession",
761-
"anonymous",
762-
"internalAuthToken",
763-
"internal",
764-
"custom",
765-
] as const satisfies readonly (keyof AuthenticationModeOptions)[];
766-
767-
// Type assertion to ensure all `AuthenticationModeOptions` keys are included in the `availableAuthenticationModes` array
768-
type MissingKeys = Exclude<keyof AuthenticationModeOptions, (typeof availableAuthenticationModes)[number]>;
769-
type _AssertAllKeysIncluded = [MissingKeys] extends [never] ? true : `Missing keys: ${MissingKeys}`;
770-
const _assertAllAuthKeysIncluded: _AssertAllKeysIncluded = true as _AssertAllKeysIncluded;
771-
772-
export const maybeGetAuthenticationModeOptionsFromConnectionOptions = (
773-
options: GadgetConnectionOptions | ClientOptions
774-
): AuthenticationModeOptions | undefined => {
775-
const topLevelAuthModes: AuthenticationModeOptions = {};
776-
for (const key of availableAuthenticationModes) {
777-
if (key in options) {
778-
topLevelAuthModes[key] = (options as any)[key];
779-
}
780-
}
781-
782-
if ("authenticationMode" in options && Object.keys(topLevelAuthModes).length > 0) {
783-
throw new GadgetClientError(
784-
"Declaring authentication modes at the top level and under the `authenticationMode` key at the same time is not allowed."
785-
);
786-
}
787-
788-
if ("authenticationMode" in options) {
789-
return options.authenticationMode;
790-
}
791-
792-
return topLevelAuthModes;
793-
};

0 commit comments

Comments
 (0)