castle-web-sdk 0.4.5 → 0.4.7

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/src/transport.ts DELETED
@@ -1,169 +0,0 @@
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/types.ts DELETED
@@ -1,7 +0,0 @@
1
- export type Json =
2
- | null
3
- | boolean
4
- | number
5
- | string
6
- | Json[]
7
- | { [key: string]: Json };
package/src/user.ts DELETED
@@ -1,58 +0,0 @@
1
- import { CastleError } from "./errors";
2
- import { hostRequest } from "./transport";
3
-
4
- export interface CastleUser {
5
- userId: string;
6
- username: string;
7
- isActive: boolean;
8
- }
9
-
10
- export interface CastleUserApi {
11
- getCurrent(): Promise<CastleUser>;
12
- }
13
-
14
- let currentUser: CastleUser | null = null;
15
- let currentUserPromise: Promise<CastleUser> | null = null;
16
-
17
- export const User: CastleUserApi = {
18
- getCurrent,
19
- };
20
-
21
- async function getCurrent(): Promise<CastleUser> {
22
- if (currentUser) return currentUser;
23
- currentUserPromise ??= fetchCurrentUser().then((user) => {
24
- currentUser = user;
25
- return user;
26
- });
27
- return currentUserPromise;
28
- }
29
-
30
- async function fetchCurrentUser(): Promise<CastleUser> {
31
- const operation = "User.getCurrent";
32
- const { user } = await hostRequest("user.getCurrent", {});
33
- if (!user) {
34
- throw new CastleError({
35
- code: "LOGIN_REQUIRED",
36
- message: "Log in to Castle before using User.getCurrent().",
37
- operation,
38
- });
39
- }
40
- return {
41
- userId: requiredString(user.userId, "user.userId", operation),
42
- username: requiredString(user.username, "user.username", operation),
43
- isActive: true,
44
- };
45
- }
46
-
47
- function requiredString(
48
- value: string | null | undefined,
49
- field: string,
50
- operation: string,
51
- ): string {
52
- if (typeof value === "string" && value.length > 0) return value;
53
- throw new CastleError({
54
- code: "GRAPHQL_BAD_DATA",
55
- message: `Castle response did not include ${field}.`,
56
- operation,
57
- });
58
- }