castle-web-sdk 0.4.3 → 0.4.5

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.
@@ -0,0 +1,169 @@
1
+ // Deck-side command-poster. The SDK never makes API calls or holds the auth
2
+ // token; every privileged operation becomes a `command` posted to the outer
3
+ // runtime (host), which validates it, stamps trusted context, runs the call
4
+ // with its own auth, and replies. Three host channels:
5
+ // - mobile → window.ReactNativeWebView.postMessage; host pushes responses
6
+ // back by calling window.__castleSdkHost.receive(...)
7
+ // - web → window.parent.postMessage; responses via 'message' events
8
+ // - local → the castle-web serve dev server, over runtime.ts's websocket
9
+ // Error reconstruction is uniform here so callers always get a CastleError.
10
+
11
+ import {
12
+ CASTLE_SDK_PROTOCOL,
13
+ isResponseEnvelope,
14
+ type CommandName,
15
+ type CommandParams,
16
+ type CommandResponseEnvelope,
17
+ type CommandResult,
18
+ type SerializedCommandError,
19
+ } from "./commands";
20
+ import { getCastleEmbed } from "./context";
21
+ import { CastleError } from "./errors";
22
+ import { sendLocalCommand } from "./runtime";
23
+
24
+ const REQUEST_TIMEOUT_MS = 15000;
25
+
26
+ // Interactive platform commands hold the screen while the player interacts with
27
+ // a host-native sheet (e.g. a pass purchase), so the flat data-command timeout
28
+ // is wrong for them: they get NO timeout and resolve only when the host replies.
29
+ const INTERACTIVE_COMMANDS: ReadonlySet<CommandName> = new Set<CommandName>([
30
+ "pass.offer",
31
+ ]);
32
+
33
+ type PostChannel = "mobile" | "web";
34
+
35
+ interface PendingCommand {
36
+ resolve: (env: CommandResponseEnvelope) => void;
37
+ reject: (error: Error) => void;
38
+ timeout?: ReturnType<typeof setTimeout>;
39
+ }
40
+
41
+ interface ReactNativeWebViewBridge {
42
+ postMessage: (message: string) => void;
43
+ }
44
+
45
+ declare global {
46
+ interface Window {
47
+ ReactNativeWebView?: ReactNativeWebViewBridge;
48
+ __castleSdkHost?: { receive: (message: unknown) => void };
49
+ }
50
+ }
51
+
52
+ let nextRequestId = 1;
53
+ const pending = new Map<string, PendingCommand>();
54
+ let listenersInstalled = false;
55
+
56
+ export async function hostRequest<C extends CommandName>(
57
+ command: C,
58
+ params: CommandParams[C],
59
+ ): Promise<CommandResult[C]> {
60
+ try {
61
+ const channel = resolveChannel();
62
+ const env =
63
+ channel === "local"
64
+ ? await sendLocalCommand(command, params)
65
+ : await postCommand(channel, command, params);
66
+ return interpretResponse(command, env) as CommandResult[C];
67
+ } catch (error) {
68
+ // Honor the SDK contract that every thrown error is a CastleError. Errors
69
+ // surfaced by the host (interpretResponse) are already CastleErrors; this
70
+ // wraps transport-level failures (host timeout / unreachable / dev server
71
+ // disconnected) that would otherwise be plain Errors.
72
+ if (error instanceof CastleError) throw error;
73
+ throw new CastleError({
74
+ code: "CASTLE_HOST_UNAVAILABLE",
75
+ message:
76
+ error instanceof Error
77
+ ? error.message
78
+ : `Castle host did not handle ${command}.`,
79
+ operation: command,
80
+ });
81
+ }
82
+ }
83
+
84
+ // Exposed so capability modules (e.g. passes) can tell whether the current host
85
+ // has its own UI surface over the deck. "mobile"/"web" hosts render their own
86
+ // purchase/upsell UI; the "local" dev server has none, so the SDK itself shows
87
+ // a minimal in-page notice there.
88
+ export function getCommandChannel(): PostChannel | "local" {
89
+ return resolveChannel();
90
+ }
91
+
92
+ function resolveChannel(): PostChannel | "local" {
93
+ if (typeof window === "undefined") return "local";
94
+ if (window.ReactNativeWebView) return "mobile";
95
+ const host = getCastleEmbed()?.host;
96
+ if (host === "web") return "web";
97
+ if (host === "dev") return "local";
98
+ // Fallback: an iframe with a parent is the web player; otherwise assume the
99
+ // local dev server (top-level page served by `castle-web serve`).
100
+ return window.parent && window.parent !== window ? "web" : "local";
101
+ }
102
+
103
+ function postCommand<C extends CommandName>(
104
+ channel: PostChannel,
105
+ command: C,
106
+ params: CommandParams[C],
107
+ ): Promise<CommandResponseEnvelope> {
108
+ installResponseListener();
109
+ const requestId = `csdk_${nextRequestId++}`;
110
+ return new Promise<CommandResponseEnvelope>((resolve, reject) => {
111
+ const timeout = INTERACTIVE_COMMANDS.has(command)
112
+ ? undefined
113
+ : setTimeout(() => {
114
+ pending.delete(requestId);
115
+ reject(new Error(`Castle host did not respond to ${command}.`));
116
+ }, REQUEST_TIMEOUT_MS);
117
+ pending.set(requestId, { resolve, reject, timeout });
118
+ sendEnvelope(channel, { castleSdk: CASTLE_SDK_PROTOCOL, requestId, command, params });
119
+ });
120
+ }
121
+
122
+ function sendEnvelope(channel: PostChannel, envelope: unknown): void {
123
+ const json = JSON.stringify(envelope);
124
+ if (channel === "mobile") {
125
+ window.ReactNativeWebView?.postMessage(json);
126
+ } else {
127
+ window.parent.postMessage(envelope, "*");
128
+ }
129
+ }
130
+
131
+ // The mobile host can't dispatch a DOM 'message' event, so it calls this global
132
+ // directly with the parsed envelope. The web host posts a 'message' event.
133
+ function installResponseListener(): void {
134
+ if (listenersInstalled || typeof window === "undefined") return;
135
+ listenersInstalled = true;
136
+ window.__castleSdkHost = { receive: (message) => settle(message) };
137
+ window.addEventListener("message", (event: MessageEvent) => {
138
+ settle(event.data);
139
+ });
140
+ }
141
+
142
+ function settle(message: unknown): void {
143
+ if (!isResponseEnvelope(message)) return;
144
+ const entry = pending.get(message.requestId);
145
+ if (!entry) return;
146
+ if (entry.timeout) clearTimeout(entry.timeout);
147
+ pending.delete(message.requestId);
148
+ entry.resolve(message);
149
+ }
150
+
151
+ function interpretResponse(
152
+ command: CommandName,
153
+ env: CommandResponseEnvelope,
154
+ ): unknown {
155
+ if (env.ok) return env.data;
156
+ throw fromSerializedError(env.error, command);
157
+ }
158
+
159
+ function fromSerializedError(
160
+ error: SerializedCommandError | undefined,
161
+ command: CommandName,
162
+ ): CastleError {
163
+ return new CastleError({
164
+ code: error?.code ?? "CASTLE_HOST_ERROR",
165
+ message: error?.message ?? `Castle command ${command} failed.`,
166
+ operation: error?.command ?? command,
167
+ extensions: error?.extensions,
168
+ });
169
+ }
package/src/user.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { requireAuthToken } from "./auth";
2
1
  import { CastleError } from "./errors";
3
- import { graphqlRequest } from "./graphql";
2
+ import { hostRequest } from "./transport";
4
3
 
5
4
  export interface CastleUser {
6
5
  userId: string;
@@ -12,22 +11,6 @@ export interface CastleUserApi {
12
11
  getCurrent(): Promise<CastleUser>;
13
12
  }
14
13
 
15
- interface CurrentUserQueryData {
16
- me: {
17
- userId?: string | null;
18
- username?: string | null;
19
- } | null;
20
- }
21
-
22
- const CURRENT_USER_QUERY = `
23
- query CastleCurrentUser {
24
- me {
25
- userId
26
- username
27
- }
28
- }
29
- `;
30
-
31
14
  let currentUser: CastleUser | null = null;
32
15
  let currentUserPromise: Promise<CastleUser> | null = null;
33
16
 
@@ -46,33 +29,17 @@ async function getCurrent(): Promise<CastleUser> {
46
29
 
47
30
  async function fetchCurrentUser(): Promise<CastleUser> {
48
31
  const operation = "User.getCurrent";
49
- const token = await requireAuthToken(operation);
50
- const data = await graphqlRequest<CurrentUserQueryData>(
51
- CURRENT_USER_QUERY,
52
- undefined,
53
- {
54
- operation,
55
- requireAuth: true,
56
- token,
57
- },
58
- );
59
- if (!data.me) {
32
+ const { user } = await hostRequest("user.getCurrent", {});
33
+ if (!user) {
60
34
  throw new CastleError({
61
35
  code: "LOGIN_REQUIRED",
62
36
  message: "Log in to Castle before using User.getCurrent().",
63
37
  operation,
64
38
  });
65
39
  }
66
- return normalizeCurrentUser(data.me, operation);
67
- }
68
-
69
- function normalizeCurrentUser(
70
- user: NonNullable<CurrentUserQueryData["me"]>,
71
- operation: string,
72
- ): CastleUser {
73
40
  return {
74
- userId: requiredString(user.userId, "me.userId", operation),
75
- username: requiredString(user.username, "me.username", operation),
41
+ userId: requiredString(user.userId, "user.userId", operation),
42
+ username: requiredString(user.username, "user.username", operation),
76
43
  isActive: true,
77
44
  };
78
45
  }
@@ -85,7 +52,7 @@ function requiredString(
85
52
  if (typeof value === "string" && value.length > 0) return value;
86
53
  throw new CastleError({
87
54
  code: "GRAPHQL_BAD_DATA",
88
- message: `Castle GraphQL response did not include ${field}.`,
55
+ message: `Castle response did not include ${field}.`,
89
56
  operation,
90
57
  });
91
58
  }
package/dist/auth.d.ts DELETED
@@ -1,8 +0,0 @@
1
- export interface CastleAuth {
2
- token?: string | null;
3
- userId?: string | null;
4
- }
5
- export declare function getAuth(): Promise<CastleAuth>;
6
- export declare function getAuthToken(): Promise<string | null>;
7
- export declare function getUserId(): Promise<string | null>;
8
- export declare function requireAuthToken(operation?: string): Promise<string>;
package/dist/auth.js DELETED
@@ -1,52 +0,0 @@
1
- import { getCastleEmbed } from "./context";
2
- import { CastleError } from "./errors";
3
- let cachedAuth = null;
4
- let cachedAuthPromise = null;
5
- export async function getAuth() {
6
- return getCachedAuth();
7
- }
8
- export async function getAuthToken() {
9
- const auth = await getCachedAuth();
10
- return auth.token ?? null;
11
- }
12
- export async function getUserId() {
13
- const auth = await getCachedAuth();
14
- return auth.userId ?? null;
15
- }
16
- export async function requireAuthToken(operation = "Castle API request") {
17
- const token = await getAuthToken();
18
- if (!token) {
19
- throw new CastleError({
20
- code: "LOGIN_REQUIRED",
21
- message: "Log in to Castle before using this API.",
22
- operation,
23
- });
24
- }
25
- return token;
26
- }
27
- function getCachedAuth() {
28
- cachedAuthPromise ??= resolveAuth();
29
- return cachedAuthPromise;
30
- }
31
- async function resolveAuth() {
32
- if (cachedAuth)
33
- return cachedAuth;
34
- const embedded = getCastleEmbed()?.auth;
35
- if (embedded && (embedded.token || embedded.userId)) {
36
- cachedAuth = { token: embedded.token, userId: embedded.userId };
37
- return cachedAuth;
38
- }
39
- try {
40
- const res = await fetch("/__castle/auth");
41
- if (res.ok) {
42
- const json = (await res.json());
43
- cachedAuth = { token: json.token, userId: json.userId };
44
- return cachedAuth;
45
- }
46
- }
47
- catch {
48
- // endpoint missing or offline -- treat as unauthenticated
49
- }
50
- cachedAuth = {};
51
- return cachedAuth;
52
- }
package/dist/graphql.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import type { Json } from "./types";
2
- export declare const GRAPHQL_ENDPOINT = "https://api.castle.xyz/graphql";
3
- export type GraphqlVariables = Record<string, Json | undefined>;
4
- export type { GraphqlErrorPayload } from "./errors";
5
- export interface GraphqlRequestOptions {
6
- endpoint?: string;
7
- headers?: Record<string, string>;
8
- operation?: string;
9
- operationName?: string;
10
- requireAuth?: boolean;
11
- signal?: AbortSignal;
12
- timeoutMs?: number;
13
- token?: string | null;
14
- }
15
- export declare function graphqlRequest<TData = unknown, TVariables extends GraphqlVariables = GraphqlVariables>(query: string, variables?: TVariables, options?: GraphqlRequestOptions): Promise<TData>;
package/dist/graphql.js DELETED
@@ -1,120 +0,0 @@
1
- import { getAuthToken } from "./auth";
2
- import { getCastleEmbed } from "./context";
3
- import { CastleError } from "./errors";
4
- export const GRAPHQL_ENDPOINT = "https://api.castle.xyz/graphql";
5
- export async function graphqlRequest(query, variables, options = {}) {
6
- const operation = options.operation ?? guessGraphqlOperation(query);
7
- const token = options.token ?? (await getAuthToken());
8
- if (options.requireAuth && !token) {
9
- throw new CastleError({
10
- code: "LOGIN_REQUIRED",
11
- message: "Log in to Castle before using this API.",
12
- operation,
13
- });
14
- }
15
- const response = await fetchGraphql(query, variables, {
16
- ...options,
17
- operation,
18
- token,
19
- });
20
- if (response.errors?.length) {
21
- throw graphqlError(response.errors, operation);
22
- }
23
- if (response.data === null || response.data === undefined) {
24
- throw new CastleError({
25
- code: "GRAPHQL_NO_DATA",
26
- message: "Castle GraphQL response did not include data.",
27
- operation,
28
- });
29
- }
30
- return response.data;
31
- }
32
- async function fetchGraphql(query, variables, options) {
33
- const headers = graphqlHeaders(options);
34
- const abort = requestAbort(options);
35
- try {
36
- const response = await fetch(graphqlEndpoint(options), {
37
- method: "POST",
38
- headers,
39
- body: JSON.stringify(graphqlBody(query, variables, options.operationName)),
40
- signal: abort.signal,
41
- });
42
- const json = (await readGraphqlJson(response, options.operation));
43
- if (!response.ok) {
44
- throw new CastleError({
45
- code: "GRAPHQL_HTTP_ERROR",
46
- message: `Castle GraphQL request failed with HTTP ${response.status}.`,
47
- operation: options.operation,
48
- status: response.status,
49
- errors: json?.errors,
50
- });
51
- }
52
- return json ?? {};
53
- }
54
- finally {
55
- abort.cleanup();
56
- }
57
- }
58
- function requestAbort(options) {
59
- if (options.signal)
60
- return { signal: options.signal, cleanup: () => { } };
61
- const controller = new AbortController();
62
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10000);
63
- return { signal: controller.signal, cleanup: () => clearTimeout(timeout) };
64
- }
65
- function graphqlEndpoint(options) {
66
- return (options.endpoint ?? getCastleEmbed()?.graphqlEndpoint ?? GRAPHQL_ENDPOINT);
67
- }
68
- function graphqlHeaders(options) {
69
- const headers = {
70
- Accept: "application/json",
71
- "Content-Type": "application/json",
72
- "X-OS": "web",
73
- "X-TimeZone": Intl.DateTimeFormat().resolvedOptions().timeZone,
74
- ...options.headers,
75
- };
76
- if (options.token)
77
- headers["X-Auth-Token"] = options.token;
78
- return headers;
79
- }
80
- function graphqlBody(query, variables, operationName) {
81
- const body = { query };
82
- if (variables)
83
- body.variables = variables;
84
- if (operationName)
85
- body.operationName = operationName;
86
- return body;
87
- }
88
- async function readGraphqlJson(response, operation) {
89
- try {
90
- return await response.json();
91
- }
92
- catch {
93
- if (response.ok)
94
- return {};
95
- throw new CastleError({
96
- code: "GRAPHQL_BAD_RESPONSE",
97
- message: "Castle GraphQL response was not valid JSON.",
98
- operation,
99
- status: response.status,
100
- });
101
- }
102
- }
103
- function graphqlError(errors, operation) {
104
- const first = errors[0];
105
- return new CastleError({
106
- code: graphqlErrorCode(first),
107
- message: first?.message ?? "Castle GraphQL request failed.",
108
- operation,
109
- extensions: first?.extensions,
110
- errors,
111
- });
112
- }
113
- function graphqlErrorCode(error) {
114
- const code = error?.extensions?.code;
115
- return typeof code === "string" ? code : "GRAPHQL_ERROR";
116
- }
117
- function guessGraphqlOperation(query) {
118
- const match = /\b(?:query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(query);
119
- return match?.[1] ?? "Castle GraphQL request";
120
- }
package/src/auth.ts DELETED
@@ -1,64 +0,0 @@
1
- import { getCastleEmbed } from "./context";
2
- import { CastleError } from "./errors";
3
-
4
- export interface CastleAuth {
5
- token?: string | null;
6
- userId?: string | null;
7
- }
8
-
9
- let cachedAuth: CastleAuth | null = null;
10
- let cachedAuthPromise: Promise<CastleAuth> | null = null;
11
-
12
- export async function getAuth(): Promise<CastleAuth> {
13
- return getCachedAuth();
14
- }
15
-
16
- export async function getAuthToken(): Promise<string | null> {
17
- const auth = await getCachedAuth();
18
- return auth.token ?? null;
19
- }
20
-
21
- export async function getUserId(): Promise<string | null> {
22
- const auth = await getCachedAuth();
23
- return auth.userId ?? null;
24
- }
25
-
26
- export async function requireAuthToken(
27
- operation = "Castle API request",
28
- ): Promise<string> {
29
- const token = await getAuthToken();
30
- if (!token) {
31
- throw new CastleError({
32
- code: "LOGIN_REQUIRED",
33
- message: "Log in to Castle before using this API.",
34
- operation,
35
- });
36
- }
37
- return token;
38
- }
39
-
40
- function getCachedAuth(): Promise<CastleAuth> {
41
- cachedAuthPromise ??= resolveAuth();
42
- return cachedAuthPromise;
43
- }
44
-
45
- async function resolveAuth(): Promise<CastleAuth> {
46
- if (cachedAuth) return cachedAuth;
47
- const embedded = getCastleEmbed()?.auth;
48
- if (embedded && (embedded.token || embedded.userId)) {
49
- cachedAuth = { token: embedded.token, userId: embedded.userId };
50
- return cachedAuth;
51
- }
52
- try {
53
- const res = await fetch("/__castle/auth");
54
- if (res.ok) {
55
- const json = (await res.json()) as CastleAuth;
56
- cachedAuth = { token: json.token, userId: json.userId };
57
- return cachedAuth;
58
- }
59
- } catch {
60
- // endpoint missing or offline -- treat as unauthenticated
61
- }
62
- cachedAuth = {};
63
- return cachedAuth;
64
- }
package/src/graphql.ts DELETED
@@ -1,182 +0,0 @@
1
- import { getAuthToken } from "./auth";
2
- import { getCastleEmbed } from "./context";
3
- import { CastleError, type GraphqlErrorPayload } from "./errors";
4
- import type { Json } from "./types";
5
-
6
- export const GRAPHQL_ENDPOINT = "https://api.castle.xyz/graphql";
7
-
8
- export type GraphqlVariables = Record<string, Json | undefined>;
9
- export type { GraphqlErrorPayload } from "./errors";
10
-
11
- export interface GraphqlRequestOptions {
12
- endpoint?: string;
13
- headers?: Record<string, string>;
14
- operation?: string;
15
- operationName?: string;
16
- requireAuth?: boolean;
17
- signal?: AbortSignal;
18
- timeoutMs?: number;
19
- token?: string | null;
20
- }
21
-
22
- interface GraphqlResponse<TData> {
23
- data?: TData;
24
- errors?: GraphqlErrorPayload[];
25
- }
26
-
27
- export async function graphqlRequest<
28
- TData = unknown,
29
- TVariables extends GraphqlVariables = GraphqlVariables,
30
- >(
31
- query: string,
32
- variables?: TVariables,
33
- options: GraphqlRequestOptions = {},
34
- ): Promise<TData> {
35
- const operation = options.operation ?? guessGraphqlOperation(query);
36
- const token = options.token ?? (await getAuthToken());
37
- if (options.requireAuth && !token) {
38
- throw new CastleError({
39
- code: "LOGIN_REQUIRED",
40
- message: "Log in to Castle before using this API.",
41
- operation,
42
- });
43
- }
44
- const response = await fetchGraphql<TData>(query, variables, {
45
- ...options,
46
- operation,
47
- token,
48
- });
49
- if (response.errors?.length) {
50
- throw graphqlError(response.errors, operation);
51
- }
52
- if (response.data === null || response.data === undefined) {
53
- throw new CastleError({
54
- code: "GRAPHQL_NO_DATA",
55
- message: "Castle GraphQL response did not include data.",
56
- operation,
57
- });
58
- }
59
- return response.data;
60
- }
61
-
62
- async function fetchGraphql<TData>(
63
- query: string,
64
- variables: GraphqlVariables | undefined,
65
- options: Required<Pick<GraphqlRequestOptions, "operation">> &
66
- GraphqlRequestOptions,
67
- ): Promise<GraphqlResponse<TData>> {
68
- const headers = graphqlHeaders(options);
69
- const abort = requestAbort(options);
70
- try {
71
- const response = await fetch(graphqlEndpoint(options), {
72
- method: "POST",
73
- headers,
74
- body: JSON.stringify(
75
- graphqlBody(query, variables, options.operationName),
76
- ),
77
- signal: abort.signal,
78
- });
79
- const json = (await readGraphqlJson(response, options.operation)) as
80
- | GraphqlResponse<TData>
81
- | undefined;
82
- if (!response.ok) {
83
- throw new CastleError({
84
- code: "GRAPHQL_HTTP_ERROR",
85
- message: `Castle GraphQL request failed with HTTP ${response.status}.`,
86
- operation: options.operation,
87
- status: response.status,
88
- errors: json?.errors,
89
- });
90
- }
91
- return json ?? {};
92
- } finally {
93
- abort.cleanup();
94
- }
95
- }
96
-
97
- function requestAbort(options: GraphqlRequestOptions): {
98
- signal?: AbortSignal;
99
- cleanup: () => void;
100
- } {
101
- if (options.signal) return { signal: options.signal, cleanup: () => {} };
102
- const controller = new AbortController();
103
- const timeout = setTimeout(
104
- () => controller.abort(),
105
- options.timeoutMs ?? 10000,
106
- );
107
- return { signal: controller.signal, cleanup: () => clearTimeout(timeout) };
108
- }
109
-
110
- function graphqlEndpoint(options: GraphqlRequestOptions): string {
111
- return (
112
- options.endpoint ?? getCastleEmbed()?.graphqlEndpoint ?? GRAPHQL_ENDPOINT
113
- );
114
- }
115
-
116
- function graphqlHeaders(
117
- options: GraphqlRequestOptions,
118
- ): Record<string, string> {
119
- const headers: Record<string, string> = {
120
- Accept: "application/json",
121
- "Content-Type": "application/json",
122
- "X-OS": "web",
123
- "X-TimeZone": Intl.DateTimeFormat().resolvedOptions().timeZone,
124
- ...options.headers,
125
- };
126
- if (options.token) headers["X-Auth-Token"] = options.token;
127
- return headers;
128
- }
129
-
130
- function graphqlBody(
131
- query: string,
132
- variables: GraphqlVariables | undefined,
133
- operationName: string | undefined,
134
- ): Record<string, unknown> {
135
- const body: Record<string, unknown> = { query };
136
- if (variables) body.variables = variables;
137
- if (operationName) body.operationName = operationName;
138
- return body;
139
- }
140
-
141
- async function readGraphqlJson(
142
- response: Response,
143
- operation: string,
144
- ): Promise<unknown> {
145
- try {
146
- return await response.json();
147
- } catch {
148
- if (response.ok) return {};
149
- throw new CastleError({
150
- code: "GRAPHQL_BAD_RESPONSE",
151
- message: "Castle GraphQL response was not valid JSON.",
152
- operation,
153
- status: response.status,
154
- });
155
- }
156
- }
157
-
158
- function graphqlError(
159
- errors: GraphqlErrorPayload[],
160
- operation: string,
161
- ): CastleError {
162
- const first = errors[0];
163
- return new CastleError({
164
- code: graphqlErrorCode(first),
165
- message: first?.message ?? "Castle GraphQL request failed.",
166
- operation,
167
- extensions: first?.extensions,
168
- errors,
169
- });
170
- }
171
-
172
- function graphqlErrorCode(error: GraphqlErrorPayload | undefined): string {
173
- const code = error?.extensions?.code;
174
- return typeof code === "string" ? code : "GRAPHQL_ERROR";
175
- }
176
-
177
- function guessGraphqlOperation(query: string): string {
178
- const match = /\b(?:query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(
179
- query,
180
- );
181
- return match?.[1] ?? "Castle GraphQL request";
182
- }