@mitway/sdk 0.2.1 → 0.2.3

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/dist/index.d.ts CHANGED
@@ -29,6 +29,135 @@ interface User {
29
29
  updated_at: string;
30
30
  }
31
31
 
32
+ /**
33
+ * Token Manager for the MITWAY-BaaS SDK.
34
+ *
35
+ * In-memory storage for the access token + user. Browser CSRF token lives
36
+ * in a cookie so the cookie-based refresh flow works across page reloads.
37
+ */
38
+
39
+ declare class TokenManager {
40
+ private accessToken;
41
+ private user;
42
+ /** Fired when the access token changes (used by long-lived consumers). */
43
+ onTokenChange: (() => void) | null;
44
+ saveSession(session: AuthSession): void;
45
+ getSession(): AuthSession | null;
46
+ getAccessToken(): string | null;
47
+ setAccessToken(token: string): void;
48
+ getUser(): User | null;
49
+ setUser(user: User): void;
50
+ clearSession(): void;
51
+ }
52
+
53
+ /**
54
+ * Realtime module — Socket.IO client wrapper for MITWAY-BaaS.
55
+ *
56
+ * Provides a thin, typed layer over `socket.io-client` so app code can
57
+ * subscribe / publish / listen without dealing with the underlying
58
+ * transport details. The MITWAY-BaaS backend handles auth (JWT / API
59
+ * key), RLS, rate limiting, and fan-out across replicas — the SDK just
60
+ * opens one socket per `MitwayBaasClient` instance and routes events.
61
+ */
62
+
63
+ /** Standardized meta envelope emitted by the server on every push. */
64
+ interface RealtimeMessageMeta {
65
+ channel?: string;
66
+ message_id: string;
67
+ sender_type: 'system' | 'user';
68
+ sender_id?: string;
69
+ timestamp: string;
70
+ }
71
+ /** Subscribe ack returned by the server. */
72
+ type SubscribeResult = {
73
+ ok: true;
74
+ channel: string;
75
+ } | {
76
+ ok: false;
77
+ channel: string;
78
+ error: {
79
+ code: string;
80
+ message: string;
81
+ };
82
+ };
83
+ /** Server-pushed unsolicited error. */
84
+ interface RealtimeErrorPayload {
85
+ channel?: string;
86
+ code: string;
87
+ message: string;
88
+ }
89
+ interface RealtimeListener<T = unknown> {
90
+ (payload: T, meta: RealtimeMessageMeta): void;
91
+ }
92
+ type ConnectionListener = () => void;
93
+ type DisconnectListener = (reason: string) => void;
94
+ type ConnectErrorListener = (error: Error) => void;
95
+ interface RealtimeOptions {
96
+ /** Override the path on the server. Defaults to the Socket.IO default. */
97
+ path?: string;
98
+ /** Transport strategy. Defaults to `['websocket']` — modern browsers
99
+ * and Node always support it, no need for the long-polling fallback. */
100
+ transports?: Array<'websocket' | 'polling'>;
101
+ /** Handshake timeout in ms. */
102
+ timeoutMs?: number;
103
+ /** Extra fields merged into socket.handshake.auth. Advanced usage. */
104
+ extraAuth?: Record<string, string>;
105
+ }
106
+ /**
107
+ * MITWAY-BaaS realtime client.
108
+ *
109
+ * Public API mirrors the InsForge realtime SDK for familiarity, but the
110
+ * wire protocol follows our backend (see
111
+ * `MITWAY-BaaS/backend/src/infra/socket/socket.manager.ts`):
112
+ *
113
+ * - Handshake auth uses `auth.token` containing the JWT (preferred)
114
+ * or an opaque API key string.
115
+ * - `realtime:subscribe` / `realtime:unsubscribe` / `realtime:publish`
116
+ * are the client-to-server events.
117
+ * - Server pushes the user-defined `event` name with `{ ...payload,
118
+ * meta }` shape.
119
+ * - `realtime:error` is the unsolicited error channel for publish
120
+ * failures and similar.
121
+ */
122
+ declare class Realtime {
123
+ private socket;
124
+ private baseUrl;
125
+ private options;
126
+ private anonKey;
127
+ private tokenManager;
128
+ private listeners;
129
+ private reserved;
130
+ private connecting;
131
+ private subscribedChannels;
132
+ constructor(baseUrl: string, tokenManager: TokenManager, anonKey: string | undefined, options?: RealtimeOptions);
133
+ get isConnected(): boolean;
134
+ get socketId(): string | undefined;
135
+ /** Explicitly open the connection. Safe to call multiple times; only
136
+ * opens one socket per instance. Subsequent calls during connection
137
+ * return the same in-flight promise. */
138
+ connect(): Promise<void>;
139
+ private openSocket;
140
+ /** Close the socket and clear in-memory subscription state. Reserved
141
+ * listeners survive so callers can reconnect later via `connect()`. */
142
+ disconnect(): void;
143
+ subscribe(channel: string): Promise<SubscribeResult>;
144
+ /** Fire-and-forget. No ack from the server. */
145
+ unsubscribe(channel: string): void;
146
+ /** Publish via the Socket.IO transport. Subject to RLS INSERT policy
147
+ * on `realtime.messages` (disabled by default — the developer must
148
+ * add a policy before clients can publish). Returns immediately; any
149
+ * server rejection comes through the `error` reserved event. */
150
+ publish(channel: string, event: string, payload: Record<string, unknown>): void;
151
+ on(event: 'connect', cb: ConnectionListener): void;
152
+ on(event: 'disconnect', cb: DisconnectListener): void;
153
+ on(event: 'connect_error', cb: ConnectErrorListener): void;
154
+ on(event: 'error', cb: RealtimeListener<RealtimeErrorPayload>): void;
155
+ on<T = unknown>(event: string, cb: RealtimeListener<T>): void;
156
+ off(event: string, cb: (...args: any[]) => void): void;
157
+ private dispatch;
158
+ private emitReserved;
159
+ }
160
+
32
161
  /**
33
162
  * MITWAY-BaaS SDK types — only SDK-specific shapes live here.
34
163
  * The `User` shape is inlined in `./lib/user` so this package has zero
@@ -89,6 +218,10 @@ interface MitwayBaasConfig {
89
218
  * @default true
90
219
  */
91
220
  autoRefreshToken?: boolean;
221
+ /**
222
+ * Realtime transport options. See `RealtimeOptions`.
223
+ */
224
+ realtime?: RealtimeOptions;
92
225
  }
93
226
  /**
94
227
  * Active user session in memory. Mirrors what the auth endpoints return.
@@ -146,27 +279,6 @@ declare class Logger {
146
279
  logResponse(method: string, url: string, status: number, durationMs: number, body?: any): void;
147
280
  }
148
281
 
149
- /**
150
- * Token Manager for the MITWAY-BaaS SDK.
151
- *
152
- * In-memory storage for the access token + user. Browser CSRF token lives
153
- * in a cookie so the cookie-based refresh flow works across page reloads.
154
- */
155
-
156
- declare class TokenManager {
157
- private accessToken;
158
- private user;
159
- /** Fired when the access token changes (used by long-lived consumers). */
160
- onTokenChange: (() => void) | null;
161
- saveSession(session: AuthSession): void;
162
- getSession(): AuthSession | null;
163
- getAccessToken(): string | null;
164
- setAccessToken(token: string): void;
165
- getUser(): User | null;
166
- setUser(user: User): void;
167
- clearSession(): void;
168
- }
169
-
170
282
  /**
171
283
  * HttpClient with retry, timeout, abort signal composition, and automatic
172
284
  * token refresh on 401 INVALID_TOKEN responses.
@@ -409,6 +521,7 @@ declare class MitwayBaasClient {
409
521
  private tokenManager;
410
522
  readonly auth: Auth;
411
523
  readonly database: Database;
524
+ readonly realtime: Realtime;
412
525
  constructor(config?: MitwayBaasConfig);
413
526
  /**
414
527
  * Escape hatch for callers that need to make custom requests against the
@@ -423,13 +536,13 @@ declare class MitwayBaasClient {
423
536
  * Currently ships:
424
537
  * - auth (signUp, signInWithPassword, signOut, refreshSession, getSession, getUser)
425
538
  * - database (PostgREST-backed query builder via @supabase/postgrest-js)
539
+ * - realtime (Socket.IO transport: subscribe / unsubscribe / publish / on)
426
540
  *
427
541
  * Not yet included (no backend support):
428
542
  * - storage
429
543
  * - functions
430
544
  * - email
431
545
  * - ai
432
- * - realtime
433
546
  *
434
547
  * @packageDocumentation
435
548
  */
@@ -449,4 +562,4 @@ declare class MitwayBaasClient {
449
562
  */
450
563
  declare function createClient(config: MitwayBaasConfig): MitwayBaasClient;
451
564
 
452
- export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, Database, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, type SignInRequest, type SignUpRequest, TokenManager, type User, createClient, MitwayBaasClient as default };
565
+ export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type ConnectErrorListener, type ConnectionListener, Database, type DisconnectListener, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, Realtime, type RealtimeErrorPayload, type RealtimeListener, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, type SubscribeResult, TokenManager, type User, createClient, MitwayBaasClient as default };
package/dist/index.js CHANGED
@@ -224,6 +224,30 @@ var TokenManager = class {
224
224
  }
225
225
  };
226
226
 
227
+ // src/lib/auth-envelope.ts
228
+ function normalizeAuthPayload(raw) {
229
+ if (!raw || typeof raw !== "object") return raw;
230
+ const src = raw;
231
+ const out = { ...src };
232
+ let mutated = false;
233
+ if ("access_token" in src && !("accessToken" in src)) {
234
+ out.accessToken = src.access_token;
235
+ delete out.access_token;
236
+ mutated = true;
237
+ }
238
+ if ("csrf_token" in src && !("csrfToken" in src)) {
239
+ out.csrfToken = src.csrf_token;
240
+ delete out.csrf_token;
241
+ mutated = true;
242
+ }
243
+ if ("refresh_token" in src && !("refreshToken" in src)) {
244
+ out.refreshToken = src.refresh_token;
245
+ delete out.refresh_token;
246
+ mutated = true;
247
+ }
248
+ return mutated ? out : raw;
249
+ }
250
+
227
251
  // src/lib/http-client.ts
228
252
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
229
253
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
@@ -419,17 +443,14 @@ var HttpClient = class {
419
443
  Date.now() - startTime,
420
444
  data
421
445
  );
422
- if (data && typeof data === "object" && "error" in data) {
423
- if (!data.statusCode && !data.status) {
424
- data.statusCode = response.status;
425
- }
426
- const error = MitwayBaasError.fromApiError(data);
427
- Object.keys(data).forEach((key) => {
428
- if (key !== "error" && key !== "message" && key !== "statusCode") {
429
- error[key] = data[key];
430
- }
431
- });
432
- throw error;
446
+ if (data && typeof data === "object" && "error" in data && data.error !== null && typeof data.error === "object") {
447
+ const envErr = data.error;
448
+ throw new MitwayBaasError(
449
+ envErr.message || response.statusText || "Request failed",
450
+ envErr.statusCode || response.status,
451
+ envErr.code || envErr.error || "REQUEST_FAILED",
452
+ envErr.nextActions
453
+ );
433
454
  }
434
455
  throw new MitwayBaasError(
435
456
  `Request failed: ${response.statusText}`,
@@ -444,6 +465,9 @@ var HttpClient = class {
444
465
  Date.now() - startTime,
445
466
  data
446
467
  );
468
+ if (data && typeof data === "object" && "data" in data && "error" in data && data.error === null) {
469
+ return data.data;
470
+ }
447
471
  return data;
448
472
  } catch (err) {
449
473
  if (timer !== void 0) clearTimeout(timer);
@@ -556,7 +580,7 @@ var HttpClient = class {
556
580
  credentials: "include"
557
581
  }
558
582
  );
559
- return response;
583
+ return normalizeAuthPayload(response);
560
584
  } finally {
561
585
  this.isRefreshing = false;
562
586
  this.refreshPromise = null;
@@ -615,11 +639,12 @@ var Auth = class {
615
639
  */
616
640
  async signUp(request) {
617
641
  try {
618
- const response = await this.http.post(
642
+ const raw = await this.http.post(
619
643
  "/api/auth/register",
620
644
  request,
621
645
  { credentials: "include" }
622
646
  );
647
+ const response = normalizeAuthPayload(raw);
623
648
  if (response?.accessToken && response.user) {
624
649
  this.saveSessionFromResponse(response);
625
650
  }
@@ -633,11 +658,12 @@ var Auth = class {
633
658
  */
634
659
  async signInWithPassword(request) {
635
660
  try {
636
- const response = await this.http.post(
661
+ const raw = await this.http.post(
637
662
  "/api/auth/login",
638
663
  request,
639
664
  { credentials: "include" }
640
665
  );
666
+ const response = normalizeAuthPayload(raw);
641
667
  if (response?.accessToken && response.user) {
642
668
  this.saveSessionFromResponse(response);
643
669
  }
@@ -828,18 +854,239 @@ var Database = class {
828
854
  }
829
855
  };
830
856
 
857
+ // src/modules/realtime.ts
858
+ import { io } from "socket.io-client";
859
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
860
+ var Realtime = class {
861
+ socket = null;
862
+ baseUrl;
863
+ options;
864
+ anonKey;
865
+ tokenManager;
866
+ listeners = /* @__PURE__ */ new Map();
867
+ reserved = {
868
+ connect: /* @__PURE__ */ new Set(),
869
+ disconnect: /* @__PURE__ */ new Set(),
870
+ connect_error: /* @__PURE__ */ new Set(),
871
+ error: /* @__PURE__ */ new Set()
872
+ };
873
+ connecting = null;
874
+ subscribedChannels = /* @__PURE__ */ new Set();
875
+ constructor(baseUrl, tokenManager, anonKey, options = {}) {
876
+ this.baseUrl = baseUrl;
877
+ this.tokenManager = tokenManager;
878
+ this.anonKey = anonKey;
879
+ this.options = options;
880
+ }
881
+ // -----------------------------------------------------------------
882
+ // Connection lifecycle
883
+ // -----------------------------------------------------------------
884
+ get isConnected() {
885
+ return this.socket?.connected === true;
886
+ }
887
+ get socketId() {
888
+ return this.socket?.id;
889
+ }
890
+ /** Explicitly open the connection. Safe to call multiple times; only
891
+ * opens one socket per instance. Subsequent calls during connection
892
+ * return the same in-flight promise. */
893
+ connect() {
894
+ if (this.isConnected) {
895
+ return Promise.resolve();
896
+ }
897
+ if (this.connecting) {
898
+ return this.connecting;
899
+ }
900
+ this.connecting = this.openSocket();
901
+ return this.connecting;
902
+ }
903
+ openSocket() {
904
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
905
+ if (!token) {
906
+ const err = new MitwayBaasError(
907
+ "Realtime requires an access token or anonKey",
908
+ 401,
909
+ "AUTH_INVALID_API_KEY"
910
+ );
911
+ this.connecting = null;
912
+ return Promise.reject(err);
913
+ }
914
+ const timeoutMs = this.options.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
915
+ const socket = io(this.baseUrl, {
916
+ path: this.options.path,
917
+ transports: this.options.transports ?? ["websocket"],
918
+ auth: { token, ...this.options.extraAuth ?? {} },
919
+ reconnection: true,
920
+ timeout: timeoutMs
921
+ });
922
+ this.socket = socket;
923
+ socket.onAny((event, ...args) => this.dispatch(event, args));
924
+ socket.on("connect", () => this.emitReserved("connect"));
925
+ socket.on("disconnect", (reason) => this.emitReserved("disconnect", reason));
926
+ socket.on("connect_error", (err) => this.emitReserved("connect_error", err));
927
+ return new Promise((resolve, reject) => {
928
+ const timer = setTimeout(() => {
929
+ socket.off("connect", onConnect);
930
+ socket.off("connect_error", onConnectError);
931
+ reject(
932
+ new MitwayBaasError(
933
+ `Realtime connection timeout after ${timeoutMs}ms`,
934
+ 408,
935
+ "CONNECTION_TIMEOUT"
936
+ )
937
+ );
938
+ this.connecting = null;
939
+ }, timeoutMs);
940
+ const clear = () => {
941
+ clearTimeout(timer);
942
+ socket.off("connect", onConnect);
943
+ socket.off("connect_error", onConnectError);
944
+ };
945
+ const onConnect = () => {
946
+ clear();
947
+ this.connecting = null;
948
+ resolve();
949
+ };
950
+ const onConnectError = (err) => {
951
+ clear();
952
+ this.connecting = null;
953
+ reject(
954
+ new MitwayBaasError(err.message, 0, "CONNECTION_FAILED")
955
+ );
956
+ };
957
+ socket.once("connect", onConnect);
958
+ socket.once("connect_error", onConnectError);
959
+ });
960
+ }
961
+ /** Close the socket and clear in-memory subscription state. Reserved
962
+ * listeners survive so callers can reconnect later via `connect()`. */
963
+ disconnect() {
964
+ if (!this.socket) {
965
+ return;
966
+ }
967
+ this.socket.disconnect();
968
+ this.socket = null;
969
+ this.subscribedChannels.clear();
970
+ }
971
+ // -----------------------------------------------------------------
972
+ // Subscribe / Unsubscribe / Publish
973
+ // -----------------------------------------------------------------
974
+ async subscribe(channel) {
975
+ await this.connect();
976
+ const socket = this.socket;
977
+ if (!socket) {
978
+ return {
979
+ ok: false,
980
+ channel,
981
+ error: { code: "NOT_CONNECTED", message: "Socket is not connected" }
982
+ };
983
+ }
984
+ const result = await new Promise((resolve) => {
985
+ socket.emit("realtime:subscribe", { channel }, (ack) => {
986
+ resolve(ack);
987
+ });
988
+ });
989
+ if (result.ok) {
990
+ this.subscribedChannels.add(channel);
991
+ }
992
+ return result;
993
+ }
994
+ /** Fire-and-forget. No ack from the server. */
995
+ unsubscribe(channel) {
996
+ this.subscribedChannels.delete(channel);
997
+ this.socket?.emit("realtime:unsubscribe", { channel });
998
+ }
999
+ /** Publish via the Socket.IO transport. Subject to RLS INSERT policy
1000
+ * on `realtime.messages` (disabled by default — the developer must
1001
+ * add a policy before clients can publish). Returns immediately; any
1002
+ * server rejection comes through the `error` reserved event. */
1003
+ publish(channel, event, payload) {
1004
+ this.socket?.emit("realtime:publish", { channel, event, payload });
1005
+ }
1006
+ // TypeScript overload impl signature must be assignable from every
1007
+ // public overload. The public overloads take arg lists of different
1008
+ // shapes (ConnectionListener: 0 args, DisconnectListener: 1 string
1009
+ // arg, RealtimeListener: 2 args), so the implementation uses the
1010
+ // widest possible signature. This matches the pattern in socket.io
1011
+ // itself and is the standard TypeScript overload idiom.
1012
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1013
+ on(event, cb) {
1014
+ if (isReserved(event)) {
1015
+ this.reserved[event].add(cb);
1016
+ return;
1017
+ }
1018
+ if (!this.listeners.has(event)) {
1019
+ this.listeners.set(event, /* @__PURE__ */ new Set());
1020
+ }
1021
+ this.listeners.get(event).add(cb);
1022
+ }
1023
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1024
+ off(event, cb) {
1025
+ if (isReserved(event)) {
1026
+ this.reserved[event].delete(cb);
1027
+ return;
1028
+ }
1029
+ this.listeners.get(event)?.delete(cb);
1030
+ }
1031
+ // -----------------------------------------------------------------
1032
+ // Internals
1033
+ // -----------------------------------------------------------------
1034
+ dispatch(event, args) {
1035
+ if (isReserved(event)) {
1036
+ return;
1037
+ }
1038
+ if (event === "realtime:error") {
1039
+ const err = args[0] ?? {};
1040
+ this.reserved.error.forEach(
1041
+ (cb) => cb(err, {
1042
+ message_id: "",
1043
+ sender_type: "system",
1044
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1045
+ })
1046
+ );
1047
+ return;
1048
+ }
1049
+ const set = this.listeners.get(event);
1050
+ if (!set || set.size === 0) {
1051
+ return;
1052
+ }
1053
+ const envelope = args[0] ?? {};
1054
+ const { meta, ...payload } = envelope;
1055
+ const metaOrStub = meta ?? {
1056
+ message_id: "",
1057
+ sender_type: "system",
1058
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1059
+ };
1060
+ set.forEach((cb) => cb(payload, metaOrStub));
1061
+ }
1062
+ emitReserved(event, ...args) {
1063
+ const set = this.reserved[event];
1064
+ set.forEach((cb) => cb(...args));
1065
+ }
1066
+ };
1067
+ function isReserved(event) {
1068
+ return event === "connect" || event === "disconnect" || event === "connect_error" || event === "error";
1069
+ }
1070
+
831
1071
  // src/client.ts
832
1072
  var MitwayBaasClient = class {
833
1073
  http;
834
1074
  tokenManager;
835
1075
  auth;
836
1076
  database;
1077
+ realtime;
837
1078
  constructor(config = {}) {
838
1079
  const logger = new Logger(config.debug);
839
1080
  this.tokenManager = new TokenManager();
840
1081
  this.http = new HttpClient(config, this.tokenManager, logger);
841
1082
  this.auth = new Auth(this.http, this.tokenManager);
842
1083
  this.database = new Database(this.http, this.tokenManager, config.anonKey);
1084
+ this.realtime = new Realtime(
1085
+ this.http.baseUrl,
1086
+ this.tokenManager,
1087
+ config.anonKey,
1088
+ config.realtime
1089
+ );
843
1090
  }
844
1091
  /**
845
1092
  * Escape hatch for callers that need to make custom requests against the
@@ -862,6 +1109,7 @@ export {
862
1109
  Logger,
863
1110
  MitwayBaasClient,
864
1111
  MitwayBaasError,
1112
+ Realtime,
865
1113
  TokenManager,
866
1114
  createClient,
867
1115
  index_default as default