@resultdev/sdk 0.2.0 → 0.3.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/dist/ssr.js CHANGED
@@ -1159,6 +1159,70 @@ var HttpClient = class {
1159
1159
  };
1160
1160
 
1161
1161
  // src/modules/ai.ts
1162
+ var SUPPORTED_AUDIO_FORMATS = [
1163
+ "wav",
1164
+ "mp3",
1165
+ "aiff",
1166
+ "aac",
1167
+ "ogg",
1168
+ "flac",
1169
+ "m4a"
1170
+ ];
1171
+ var MULTIMODAL_MODEL = "google/gemini-2.5-flash";
1172
+ function summarizeContent(messages) {
1173
+ const summary = {
1174
+ hasImage: false,
1175
+ hasAudio: false,
1176
+ badAudioFormats: []
1177
+ };
1178
+ for (const message of messages) {
1179
+ if (message.images?.length) {
1180
+ summary.hasImage = true;
1181
+ }
1182
+ if (!Array.isArray(message.content)) {
1183
+ continue;
1184
+ }
1185
+ for (const part of message.content) {
1186
+ if (part?.type === "image_url") {
1187
+ summary.hasImage = true;
1188
+ } else if (part?.type === "input_audio") {
1189
+ summary.hasAudio = true;
1190
+ const format2 = part.input_audio?.format;
1191
+ if (format2 && !SUPPORTED_AUDIO_FORMATS.includes(format2)) {
1192
+ summary.badAudioFormats.push(format2);
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ return summary;
1198
+ }
1199
+ function assertSupportedContent(model, summary) {
1200
+ if (summary.badAudioFormats.length) {
1201
+ throw new ResultError(
1202
+ `Audio format "${summary.badAudioFormats[0]}" is not supported. Supported formats: ${SUPPORTED_AUDIO_FORMATS.join(", ")}. Browser MediaRecorder produces webm/opus, which the gateway rejects - capture PCM with the Web Audio API and encode a 16 kHz mono WAV instead.`,
1203
+ 400,
1204
+ "AI_INVALID_INPUT"
1205
+ );
1206
+ }
1207
+ if (summary.hasAudio && model.startsWith("openai/")) {
1208
+ throw new ResultError(
1209
+ `Model "${model}" does not accept audio input on this gateway. Use ${MULTIMODAL_MODEL} (verified for audio).`,
1210
+ 400,
1211
+ "AI_INVALID_INPUT"
1212
+ );
1213
+ }
1214
+ }
1215
+ function enrichUpstreamError(error, summary) {
1216
+ if (!(error instanceof ResultError) || error.error !== "AI_UPSTREAM_UNAVAILABLE" || !summary.hasImage && !summary.hasAudio) {
1217
+ return error;
1218
+ }
1219
+ const kind = summary.hasImage && summary.hasAudio ? "image and audio" : summary.hasImage ? "image" : "audio";
1220
+ return new ResultError(
1221
+ `${error.message} This request included ${kind} input; if the error persists, use ${MULTIMODAL_MODEL} (verified for vision and audio).`,
1222
+ error.statusCode,
1223
+ error.error
1224
+ );
1225
+ }
1162
1226
  var AI = class {
1163
1227
  chat;
1164
1228
  images;
@@ -1181,6 +1245,8 @@ var ChatCompletions = class {
1181
1245
  }
1182
1246
  http;
1183
1247
  async create(params) {
1248
+ const contentSummary = summarizeContent(params.messages ?? []);
1249
+ assertSupportedContent(params.model, contentSummary);
1184
1250
  const backendParams = {
1185
1251
  model: params.model,
1186
1252
  messages: params.messages,
@@ -1209,15 +1275,28 @@ var ChatCompletions = class {
1209
1275
  }
1210
1276
  );
1211
1277
  if (!response2.ok) {
1212
- const error = await response2.json();
1213
- throw new Error(error.error || "Stream request failed");
1278
+ let body = null;
1279
+ try {
1280
+ body = await response2.json();
1281
+ } catch {
1282
+ }
1283
+ throw enrichUpstreamError(
1284
+ new ResultError(
1285
+ body?.message || body?.error || "Stream request failed",
1286
+ body?.statusCode ?? response2.status,
1287
+ body?.error || "AI_STREAM_ERROR"
1288
+ ),
1289
+ contentSummary
1290
+ );
1214
1291
  }
1215
1292
  return this.parseSSEStream(response2, params.model);
1216
1293
  }
1217
- const response = await this.http.post(
1218
- "/api/ai/chat/completion",
1219
- backendParams
1220
- );
1294
+ let response;
1295
+ try {
1296
+ response = await this.http.post("/api/ai/chat/completion", backendParams);
1297
+ } catch (error) {
1298
+ throw enrichUpstreamError(error, contentSummary);
1299
+ }
1221
1300
  const content = response.text || "";
1222
1301
  return {
1223
1302
  id: `chatcmpl-${Date.now()}`,
@@ -1312,6 +1391,37 @@ var ChatCompletions = class {
1312
1391
  ]
1313
1392
  };
1314
1393
  }
1394
+ if (data.annotations?.length) {
1395
+ yield {
1396
+ id: `chatcmpl-${Date.now()}`,
1397
+ object: "chat.completion.chunk",
1398
+ created: Math.floor(Date.now() / 1e3),
1399
+ model,
1400
+ choices: [
1401
+ {
1402
+ index: 0,
1403
+ delta: {
1404
+ annotations: data.annotations
1405
+ },
1406
+ finish_reason: null
1407
+ }
1408
+ ]
1409
+ };
1410
+ }
1411
+ if (data.tokenUsage) {
1412
+ yield {
1413
+ id: `chatcmpl-${Date.now()}`,
1414
+ object: "chat.completion.chunk",
1415
+ created: Math.floor(Date.now() / 1e3),
1416
+ model,
1417
+ choices: [],
1418
+ usage: {
1419
+ prompt_tokens: data.tokenUsage.promptTokens || 0,
1420
+ completion_tokens: data.tokenUsage.completionTokens || 0,
1421
+ total_tokens: data.tokenUsage.totalTokens || 0
1422
+ }
1423
+ };
1424
+ }
1315
1425
  if (data.done) {
1316
1426
  reader.releaseLock();
1317
1427
  return;
@@ -2548,13 +2658,13 @@ var Functions = class _Functions {
2548
2658
  functionsUrl;
2549
2659
  constructor(http, functionsUrl) {
2550
2660
  this.http = http;
2551
- this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
2661
+ this.functionsUrl = functionsUrl;
2552
2662
  }
2553
2663
  /**
2554
- * Derive the subhosting URL from the base URL.
2555
- * Rewrites the backend base URL to its functions subhosting URL
2664
+ * Derive the legacy subhosting URL from the base URL
2556
2665
  * ({appKey}.{region}.<domain> -> {appKey}.functions.<domain>).
2557
- * Only applies to platform-hosted backends.
2666
+ * Only used to recognize when a configured functionsUrl still points at
2667
+ * the local deployment, so in-process dispatch can short-circuit it.
2558
2668
  */
2559
2669
  static deriveSubhostingUrl(baseUrl) {
2560
2670
  try {
@@ -2587,11 +2697,13 @@ var Functions = class _Functions {
2587
2697
  * Invoke an Edge Function.
2588
2698
  *
2589
2699
  * Dispatch order:
2590
- * 1. If the platform's in-process dispatch hook is present, call it directly.
2591
- * This avoids Deno Subhosting's 508 Loop Detected when one bundled
2592
- * function invokes another inside the same deployment.
2593
- * 2. Otherwise, try the configured subhosting URL.
2594
- * 3. On 404 from subhosting, fall back to the proxy path.
2700
+ * 1. If the platform's in-process dispatch hook is present and no foreign
2701
+ * functionsUrl is configured, call it directly (function-to-function
2702
+ * calls inside the same deployment).
2703
+ * 2. If a custom functionsUrl is configured, try it; fall back to the
2704
+ * proxy path on 404 or network failure.
2705
+ * 3. Otherwise, invoke through the main host's /functions/{slug} proxy.
2706
+ * This route works from browsers too (correct CORS).
2595
2707
  *
2596
2708
  * @param slug The function slug to invoke
2597
2709
  * @param options Request options
@@ -2600,7 +2712,7 @@ var Functions = class _Functions {
2600
2712
  const { method = "POST", body, headers = {} } = options;
2601
2713
  const dispatch = globalThis.__insforge_dispatch__;
2602
2714
  const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
2603
- if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
2715
+ if (typeof dispatch === "function" && (this.functionsUrl === void 0 || !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl)) {
2604
2716
  try {
2605
2717
  const req = this.buildInProcessRequest(slug, method, body, headers);
2606
2718
  const res = await dispatch(req);
@@ -2635,7 +2747,8 @@ var Functions = class _Functions {
2635
2747
  if (error instanceof Error && error.name === "AbortError") {
2636
2748
  throw error;
2637
2749
  }
2638
- if (error instanceof ResultError && error.statusCode === 404) {
2750
+ const unreachable = error instanceof ResultError && (error.statusCode === 404 || error.statusCode === 0);
2751
+ if (unreachable) {
2639
2752
  } else {
2640
2753
  return {
2641
2754
  data: null,
@@ -2671,6 +2784,17 @@ var Functions = class _Functions {
2671
2784
  // src/modules/realtime.ts
2672
2785
  var CONNECT_TIMEOUT = 1e4;
2673
2786
  var SUBSCRIBE_TIMEOUT = 1e4;
2787
+ var CHANNEL_PREFIX = "realtime:";
2788
+ function normalizeChannelMeta(message) {
2789
+ const channel = message?.meta?.channel;
2790
+ if (typeof channel !== "string" || !channel.startsWith(CHANNEL_PREFIX)) {
2791
+ return message;
2792
+ }
2793
+ return {
2794
+ ...message,
2795
+ meta: { ...message.meta, channel: channel.slice(CHANNEL_PREFIX.length) }
2796
+ };
2797
+ }
2674
2798
  var Realtime = class {
2675
2799
  constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2676
2800
  this.baseUrl = baseUrl;
@@ -2792,8 +2916,9 @@ var Realtime = class {
2792
2916
  if (event === "realtime:error") {
2793
2917
  return;
2794
2918
  }
2795
- this.applyPresenceEvent(event, message);
2796
- this.notifyListeners(event, message);
2919
+ const normalized = normalizeChannelMeta(message);
2920
+ this.applyPresenceEvent(event, normalized);
2921
+ this.notifyListeners(event, normalized);
2797
2922
  };
2798
2923
  this.connectionAttempt = { id: attemptId, socket, cancel: fail };
2799
2924
  socket.on("connect", onConnect);
@@ -3041,6 +3166,17 @@ var Realtime = class {
3041
3166
  this.socket.emit("realtime:unsubscribe", { channel });
3042
3167
  }
3043
3168
  }
3169
+ /**
3170
+ * Publish an event to a channel.
3171
+ *
3172
+ * Delivery notes (verified against a live backend):
3173
+ * - Payload fields arrive spread onto the received message itself, next to
3174
+ * `meta` - not nested under a `payload` key. Publishing `{ text: "hi" }`
3175
+ * means receivers read `msg.text`.
3176
+ * - The sender receives its own message too. Deduplicate with
3177
+ * `msg.meta.messageId` (or `msg.meta.senderId`) if you also update local
3178
+ * state optimistically.
3179
+ */
3044
3180
  async publish(channel, event, payload) {
3045
3181
  if (!this.socket?.connected) {
3046
3182
  throw new Error(
@@ -3049,6 +3185,12 @@ var Realtime = class {
3049
3185
  }
3050
3186
  this.socket.emit("realtime:publish", { channel, event, payload });
3051
3187
  }
3188
+ /**
3189
+ * Listen for an event by name, across all subscribed channels.
3190
+ * Filter by channel inside the callback: `msg.meta.channel` always matches
3191
+ * the exact name passed to subscribe() (the SDK strips the server's
3192
+ * transport prefix before delivery).
3193
+ */
3052
3194
  on(event, callback) {
3053
3195
  const listeners = this.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
3054
3196
  listeners.add(callback);
@@ -498,13 +498,21 @@ interface RealtimeErrorPayload {
498
498
  message: string;
499
499
  }
500
500
  interface SocketMessageMeta {
501
- /** Present for room broadcasts. */
501
+ /**
502
+ * Present for room broadcasts. Matches the exact channel name passed to
503
+ * subscribe() - the SDK strips the server's transport prefix on delivery.
504
+ */
502
505
  channel?: string;
503
506
  messageId: string;
504
507
  senderType: SenderType;
505
508
  senderId?: string;
506
509
  timestamp: string;
507
510
  }
511
+ /**
512
+ * A received realtime message. The published payload's fields arrive spread
513
+ * onto this object next to `meta` (publish `{ text: "hi" }`, read
514
+ * `msg.text`) - there is no `payload` wrapper key.
515
+ */
508
516
  interface SocketMessage {
509
517
  meta: SocketMessageMeta;
510
518
  [key: string]: unknown;
@@ -498,13 +498,21 @@ interface RealtimeErrorPayload {
498
498
  message: string;
499
499
  }
500
500
  interface SocketMessageMeta {
501
- /** Present for room broadcasts. */
501
+ /**
502
+ * Present for room broadcasts. Matches the exact channel name passed to
503
+ * subscribe() - the SDK strips the server's transport prefix on delivery.
504
+ */
502
505
  channel?: string;
503
506
  messageId: string;
504
507
  senderType: SenderType;
505
508
  senderId?: string;
506
509
  timestamp: string;
507
510
  }
511
+ /**
512
+ * A received realtime message. The published payload's fields arrive spread
513
+ * onto this object next to `meta` (publish `{ text: "hi" }`, read
514
+ * `msg.text`) - there is no `payload` wrapper key.
515
+ */
508
516
  interface SocketMessage {
509
517
  meta: SocketMessageMeta;
510
518
  [key: string]: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultdev/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Result Backend SDK - authentication, database, storage, functions, realtime, AI, email, web analytics and customer support for apps built on Result.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://docs.result.dev",