@freqhole/api-client 0.2.15 → 0.3.1

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.
@@ -6,20 +6,20 @@
6
6
  import { z } from "zod";
7
7
  import type { Transport, BlobData } from "./transport.js";
8
8
  import { HttpTransport } from "./transport.js";
9
+ import { AUTH_ERROR_PATH, toZodError } from "./errors.js";
9
10
  import { createAdminMethods } from "./domains/admin.js";
10
11
  import { createAppMethods } from "./domains/app.js";
11
12
  import { createAuthMethods } from "./domains/auth.js";
13
+ import { createEntitiesMethods } from "./domains/entities.js";
12
14
  import { createJobsMethods } from "./domains/jobs.js";
13
15
  import { createMusicMethods } from "./domains/music.js";
14
16
  import { createUploadMethods } from "./domains/upload.js";
17
+ import { createVideoMethods } from "./domains/video.js";
15
18
  import type { CallFn, SafeParseResult } from "./domains/types.js";
16
19
 
17
20
  // re-export types for consumers
18
21
  export type { SafeParseResult } from "./domains/types.js";
19
22
 
20
- // sentinel for 401 detection
21
- const AUTH_ERROR_PATH = "__auth_expired__";
22
-
23
23
  // ============================================================================
24
24
  // FreqholeClient
25
25
  // ============================================================================
@@ -28,18 +28,22 @@ export class FreqholeClient {
28
28
  public readonly admin: ReturnType<typeof createAdminMethods>;
29
29
  public readonly app: ReturnType<typeof createAppMethods>;
30
30
  public readonly auth: ReturnType<typeof createAuthMethods>;
31
+ public readonly entities: ReturnType<typeof createEntitiesMethods>;
31
32
  public readonly jobs: ReturnType<typeof createJobsMethods>;
32
33
  public readonly music: ReturnType<typeof createMusicMethods>;
33
34
  public readonly upload: ReturnType<typeof createUploadMethods>;
35
+ public readonly video: ReturnType<typeof createVideoMethods>;
34
36
 
35
37
  constructor(public readonly transport: Transport) {
36
38
  const call = this.createCallFn();
37
39
  this.admin = createAdminMethods(call);
38
40
  this.app = createAppMethods(call);
39
41
  this.auth = createAuthMethods(call);
42
+ this.entities = createEntitiesMethods(call);
40
43
  this.jobs = createJobsMethods(transport);
41
44
  this.music = createMusicMethods(call);
42
45
  this.upload = createUploadMethods(transport);
46
+ this.video = createVideoMethods(call);
43
47
  }
44
48
 
45
49
  // -------------------------------------------------------------------------
@@ -58,7 +62,14 @@ export class FreqholeClient {
58
62
  ): Promise<SafeParseResult<Resp>> => {
59
63
  // validate request body for POST/PUT/PATCH
60
64
  const isFormData = params instanceof FormData;
61
- if (method !== "GET" && method !== "DELETE" && method !== "HEAD" && reqSchema && params && !isFormData) {
65
+ if (
66
+ method !== "GET" &&
67
+ method !== "DELETE" &&
68
+ method !== "HEAD" &&
69
+ reqSchema &&
70
+ params &&
71
+ !isFormData
72
+ ) {
62
73
  const validated = reqSchema.safeParse(params);
63
74
  if (!validated.success) {
64
75
  return { success: false, error: validated.error };
@@ -75,51 +86,17 @@ export class FreqholeClient {
75
86
  }
76
87
 
77
88
  // prepare body
78
- const body = method !== "GET" && method !== "DELETE" && method !== "HEAD" && params && !isFormData
79
- ? JSON.stringify(params)
80
- : undefined;
89
+ const body =
90
+ method !== "GET" && method !== "DELETE" && method !== "HEAD" && params && !isFormData
91
+ ? JSON.stringify(params)
92
+ : undefined;
81
93
 
82
94
  try {
83
95
  const response = await this.transport.request(method, finalPath, body);
84
96
 
85
97
  // handle errors: status >= 400 OR status 0 (IPC/network failure)
86
98
  if (response.status >= 400 || response.status === 0) {
87
- // try to extract error details
88
- let errorMessage = response.status === 0 ? "connection error" : `HTTP ${response.status}`;
89
- let errorCode: string | undefined;
90
- try {
91
- const errorBody = JSON.parse(response.body);
92
- if (errorBody?.error) {
93
- errorMessage = response.status === 0
94
- ? errorBody.error
95
- : `HTTP ${response.status}: ${errorBody.error}`;
96
- }
97
- if (errorBody?.message) {
98
- errorMessage = response.status === 0
99
- ? errorBody.message
100
- : `HTTP ${response.status}: ${errorBody.message}`;
101
- }
102
- if (errorBody?.code) {
103
- errorCode = errorBody.code;
104
- }
105
- } catch {
106
- // body wasn't JSON
107
- }
108
-
109
- const issuePath: (string | number)[] = [];
110
- if (response.status === 401) {
111
- issuePath.push(AUTH_ERROR_PATH);
112
- }
113
- if (errorCode) {
114
- issuePath.push(errorCode);
115
- }
116
-
117
- return {
118
- success: false,
119
- error: new z.ZodError([
120
- { code: "custom", path: issuePath, message: errorMessage },
121
- ]),
122
- };
99
+ return { success: false, error: toZodError(response.body, response.status) };
123
100
  }
124
101
 
125
102
  // no response schema (e.g., blob streaming)
@@ -132,21 +109,11 @@ export class FreqholeClient {
132
109
 
133
110
  // check for GrimoireResponse failure (success: false with errors)
134
111
  if (json.success === false) {
135
- const errorMessage = json.message || json.errors?.[0]?.detail || "request failed";
136
- const errorCode = json.errors?.[0]?.error_type;
137
- const issuePath: (string | number)[] = [];
138
- if (errorCode === "unauthorized") {
139
- issuePath.push(AUTH_ERROR_PATH);
140
- }
141
- if (errorCode) {
142
- issuePath.push(errorCode);
143
- }
144
- return {
145
- success: false,
146
- error: new z.ZodError([
147
- { code: "custom", path: issuePath, message: errorMessage },
148
- ]),
149
- };
112
+ // this used to be silent - a business-logic failure (e.g. "video
113
+ // not found") would just surface as `null` data to the caller
114
+ // with no trace anywhere, making it look like a data-wiring bug.
115
+ console.warn(`[API] ${domain}.${routeName} failed:`, json.message, json.errors);
116
+ return { success: false, error: toZodError(response.body, response.status) };
150
117
  }
151
118
 
152
119
  const data = json.data ?? json;
@@ -176,9 +143,7 @@ export class FreqholeClient {
176
143
  }
177
144
  return {
178
145
  success: false,
179
- error: new z.ZodError([
180
- { code: "custom", path: [], message },
181
- ]),
146
+ error: new z.ZodError([{ code: "custom", path: [], message }]),
182
147
  };
183
148
  }
184
149
  };
@@ -236,26 +201,24 @@ export function isAuthError<T>(result: SafeParseResult<T>): boolean {
236
201
 
237
202
  export function isNetworkError<T>(result: SafeParseResult<T>): boolean {
238
203
  if (result.success) return false;
239
- return result.error.issues.some(
240
- (issue) => {
241
- if (issue.code !== "custom") return false;
242
- const msg = issue.message.toLowerCase();
243
-
244
- // HTTP fetch errors
245
- if (msg === "failed to fetch" || msg === "network error") return true;
246
-
247
- // P2P/iroh connection errors - be generous with matching
248
- if (msg.includes("connection")) return true; // connection failed, closed, refused, etc
249
- if (msg.includes("connect to peer")) return true; // "failed to connect to peer ..."
250
- if (msg.includes("federation api error")) return true; // wrapper from p2p_client
251
- if (msg.includes("no addressing information")) return true; // iroh: peer has no relay/direct addrs
252
- if (msg.includes("timeout") || msg.includes("unreachable")) return true;
253
- if (msg.includes("closed")) return true; // ClosedPath, stream closed, etc
254
- if (msg.includes("no route") || msg.includes("endpoint")) return true;
255
- if (msg.includes("stream") && (msg.includes("error") || msg.includes("failed"))) return true;
256
- if (msg.includes("read") && msg.includes("error")) return true;
257
-
258
- return false;
259
- }
260
- );
204
+ return result.error.issues.some((issue) => {
205
+ if (issue.code !== "custom") return false;
206
+ const msg = issue.message.toLowerCase();
207
+
208
+ // HTTP fetch errors
209
+ if (msg === "failed to fetch" || msg === "network error") return true;
210
+
211
+ // P2P/iroh connection errors - be generous with matching
212
+ if (msg.includes("connection")) return true; // connection failed, closed, refused, etc
213
+ if (msg.includes("connect to peer")) return true; // "failed to connect to peer ..."
214
+ if (msg.includes("federation api error")) return true; // wrapper from p2p_client
215
+ if (msg.includes("no addressing information")) return true; // iroh: peer has no relay/direct addrs
216
+ if (msg.includes("timeout") || msg.includes("unreachable")) return true;
217
+ if (msg.includes("closed")) return true; // ClosedPath, stream closed, etc
218
+ if (msg.includes("no route") || msg.includes("endpoint")) return true;
219
+ if (msg.includes("stream") && (msg.includes("error") || msg.includes("failed"))) return true;
220
+ if (msg.includes("read") && msg.includes("error")) return true;
221
+
222
+ return false;
223
+ });
261
224
  }