vibeusage 0.2.23 → 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.
Files changed (143) hide show
  1. package/README.md +25 -20
  2. package/README.zh-CN.md +7 -2
  3. package/node_modules/@insforge/sdk/LICENSE +201 -201
  4. package/node_modules/@insforge/sdk/README.md +326 -259
  5. package/node_modules/@insforge/sdk/dist/index.d.mts +377 -182
  6. package/node_modules/@insforge/sdk/dist/index.d.ts +377 -182
  7. package/node_modules/@insforge/sdk/dist/index.js +1172 -677
  8. package/node_modules/@insforge/sdk/dist/index.js.map +1 -1
  9. package/node_modules/@insforge/sdk/dist/index.mjs +1171 -677
  10. package/node_modules/@insforge/sdk/dist/index.mjs.map +1 -1
  11. package/node_modules/@insforge/sdk/package.json +68 -68
  12. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.d.ts +1120 -43
  13. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.d.ts.map +1 -1
  14. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.js +179 -5
  15. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.js.map +1 -1
  16. package/node_modules/@insforge/shared-schemas/dist/ai.schema.d.ts +25 -25
  17. package/node_modules/@insforge/shared-schemas/dist/ai.schema.d.ts.map +1 -1
  18. package/node_modules/@insforge/shared-schemas/dist/ai.schema.js +2 -2
  19. package/node_modules/@insforge/shared-schemas/dist/ai.schema.js.map +1 -1
  20. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.d.ts +197 -51
  21. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.d.ts.map +1 -1
  22. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.js +87 -23
  23. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.js.map +1 -1
  24. package/node_modules/@insforge/shared-schemas/dist/auth.schema.d.ts +32 -3
  25. package/node_modules/@insforge/shared-schemas/dist/auth.schema.d.ts.map +1 -1
  26. package/node_modules/@insforge/shared-schemas/dist/auth.schema.js +21 -3
  27. package/node_modules/@insforge/shared-schemas/dist/auth.schema.js.map +1 -1
  28. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.d.ts +380 -0
  29. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.d.ts.map +1 -1
  30. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.js +74 -0
  31. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.js.map +1 -1
  32. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.d.ts +13 -13
  33. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.js +1 -1
  34. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.js.map +1 -1
  35. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.d.ts +735 -0
  36. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.d.ts.map +1 -0
  37. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.js +209 -0
  38. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.js.map +1 -0
  39. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.d.ts +37 -0
  40. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.d.ts.map +1 -0
  41. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.js +25 -0
  42. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.js.map +1 -0
  43. package/node_modules/@insforge/shared-schemas/dist/docs.schema.d.ts +5 -1
  44. package/node_modules/@insforge/shared-schemas/dist/docs.schema.d.ts.map +1 -1
  45. package/node_modules/@insforge/shared-schemas/dist/docs.schema.js +34 -4
  46. package/node_modules/@insforge/shared-schemas/dist/docs.schema.js.map +1 -1
  47. package/node_modules/@insforge/shared-schemas/dist/email-api.schema.js +1 -1
  48. package/node_modules/@insforge/shared-schemas/dist/email-api.schema.js.map +1 -1
  49. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.d.ts +186 -6
  50. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.d.ts.map +1 -1
  51. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.js +21 -2
  52. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.js.map +1 -1
  53. package/node_modules/@insforge/shared-schemas/dist/functions.schema.d.ts +5 -5
  54. package/node_modules/@insforge/shared-schemas/dist/functions.schema.js +1 -1
  55. package/node_modules/@insforge/shared-schemas/dist/functions.schema.js.map +1 -1
  56. package/node_modules/@insforge/shared-schemas/dist/index.d.ts +24 -18
  57. package/node_modules/@insforge/shared-schemas/dist/index.d.ts.map +1 -1
  58. package/node_modules/@insforge/shared-schemas/dist/index.js +24 -18
  59. package/node_modules/@insforge/shared-schemas/dist/index.js.map +1 -1
  60. package/node_modules/@insforge/shared-schemas/dist/logs-api.schema.js +1 -1
  61. package/node_modules/@insforge/shared-schemas/dist/logs-api.schema.js.map +1 -1
  62. package/node_modules/@insforge/shared-schemas/dist/logs.schema.d.ts +43 -0
  63. package/node_modules/@insforge/shared-schemas/dist/logs.schema.d.ts.map +1 -1
  64. package/node_modules/@insforge/shared-schemas/dist/logs.schema.js +11 -0
  65. package/node_modules/@insforge/shared-schemas/dist/logs.schema.js.map +1 -1
  66. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.d.ts +229 -172
  67. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.d.ts.map +1 -1
  68. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.js +27 -7
  69. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.js.map +1 -1
  70. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.d.ts +51 -0
  71. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.d.ts.map +1 -0
  72. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.js +31 -0
  73. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.js.map +1 -0
  74. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.d.ts +31 -0
  75. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.d.ts.map +1 -0
  76. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.js +12 -0
  77. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.js.map +1 -0
  78. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.d.ts +39 -20
  79. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.d.ts.map +1 -1
  80. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.js +5 -1
  81. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.js.map +1 -1
  82. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.d.ts +12 -4
  83. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.d.ts.map +1 -1
  84. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.js +6 -0
  85. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.js.map +1 -1
  86. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.d.ts +287 -0
  87. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.d.ts.map +1 -0
  88. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.js +81 -0
  89. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.js.map +1 -0
  90. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.d.ts +77 -0
  91. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.d.ts.map +1 -0
  92. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.js +36 -0
  93. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.js.map +1 -0
  94. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.d.ts +113 -0
  95. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.d.ts.map +1 -0
  96. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.js +31 -0
  97. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.js.map +1 -0
  98. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.d.ts +31 -0
  99. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.d.ts.map +1 -0
  100. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.js +13 -0
  101. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.js.map +1 -0
  102. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.d.ts +27 -2
  103. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.d.ts.map +1 -1
  104. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.js +9 -1
  105. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.js.map +1 -1
  106. package/node_modules/@insforge/shared-schemas/dist/storage.schema.d.ts +17 -0
  107. package/node_modules/@insforge/shared-schemas/dist/storage.schema.d.ts.map +1 -1
  108. package/node_modules/@insforge/shared-schemas/dist/storage.schema.js +6 -0
  109. package/node_modules/@insforge/shared-schemas/dist/storage.schema.js.map +1 -1
  110. package/node_modules/@insforge/shared-schemas/package.json +2 -1
  111. package/package.json +5 -6
  112. package/src/cli.js +2 -2
  113. package/src/commands/init.js +20 -362
  114. package/src/commands/status.js +58 -51
  115. package/src/commands/sync.js +37 -25
  116. package/src/commands/uninstall.js +121 -104
  117. package/src/lib/claude-config.js +130 -35
  118. package/src/lib/diagnostics.js +88 -57
  119. package/src/lib/doctor.js +50 -0
  120. package/src/lib/insforge-client.js +13 -9
  121. package/src/lib/integrations/claude.js +106 -0
  122. package/src/lib/integrations/codex.js +88 -0
  123. package/src/lib/integrations/context.js +76 -0
  124. package/src/lib/integrations/every-code.js +88 -0
  125. package/src/lib/integrations/gemini.js +86 -0
  126. package/src/lib/integrations/index.js +85 -0
  127. package/src/lib/integrations/openclaw-legacy.js +123 -0
  128. package/src/lib/integrations/openclaw-session.js +132 -0
  129. package/src/lib/integrations/opencode.js +86 -0
  130. package/src/lib/integrations/utils.js +39 -0
  131. package/src/lib/opencode-sqlite.js +113 -0
  132. package/src/lib/opencode-usage-audit.js +3 -2
  133. package/src/lib/rollout.js +227 -1
  134. package/src/lib/runtime-config.js +7 -5
  135. package/src/lib/vibeusage-api.js +11 -7
  136. package/src/shared/copy-registry.cjs +142 -0
  137. package/src/shared/copy-registry.cjs.d.ts +33 -0
  138. package/src/shared/runtime-defaults.cjs +11 -0
  139. package/src/shared/runtime-defaults.cjs.d.ts +3 -0
  140. package/src/shared/vibeusage-function-contract.cjs +34 -0
  141. package/src/shared/vibeusage-function-contract.cjs.d.ts +4 -0
  142. package/src/commands/activate-if-needed.js +0 -41
  143. package/src/lib/activation-check.js +0 -341
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  HttpClient: () => HttpClient,
29
29
  InsForgeClient: () => InsForgeClient,
30
30
  InsForgeError: () => InsForgeError,
31
+ Logger: () => Logger,
31
32
  Realtime: () => Realtime,
32
33
  Storage: () => Storage,
33
34
  StorageBucket: () => StorageBucket,
@@ -56,127 +57,182 @@ var InsForgeError = class _InsForgeError extends Error {
56
57
  }
57
58
  };
58
59
 
59
- // src/lib/http-client.ts
60
- var HttpClient = class {
61
- constructor(config) {
62
- this.userToken = null;
63
- this.baseUrl = config.baseUrl || "http://localhost:7130";
64
- this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
65
- this.anonKey = config.anonKey;
66
- this.defaultHeaders = {
67
- ...config.headers
68
- };
69
- if (!this.fetch) {
70
- throw new Error(
71
- "Fetch is not available. Please provide a fetch implementation in the config."
72
- );
60
+ // src/lib/logger.ts
61
+ var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
62
+ var SENSITIVE_BODY_KEYS = [
63
+ "password",
64
+ "token",
65
+ "accesstoken",
66
+ "refreshtoken",
67
+ "authorization",
68
+ "secret",
69
+ "apikey",
70
+ "api_key",
71
+ "email",
72
+ "ssn",
73
+ "creditcard",
74
+ "credit_card"
75
+ ];
76
+ function redactHeaders(headers) {
77
+ const redacted = {};
78
+ for (const [key, value] of Object.entries(headers)) {
79
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
80
+ redacted[key] = "***REDACTED***";
81
+ } else {
82
+ redacted[key] = value;
73
83
  }
74
84
  }
75
- buildUrl(path, params) {
76
- const url = new URL(path, this.baseUrl);
77
- if (params) {
78
- Object.entries(params).forEach(([key, value]) => {
79
- if (key === "select") {
80
- let normalizedValue = value.replace(/\s+/g, " ").trim();
81
- normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
82
- url.searchParams.append(key, normalizedValue);
83
- } else {
84
- url.searchParams.append(key, value);
85
- }
86
- });
85
+ return redacted;
86
+ }
87
+ function sanitizeBody(body) {
88
+ if (body === null || body === void 0) return body;
89
+ if (typeof body === "string") {
90
+ try {
91
+ const parsed = JSON.parse(body);
92
+ return sanitizeBody(parsed);
93
+ } catch {
94
+ return body;
87
95
  }
88
- return url.toString();
89
96
  }
90
- async request(method, path, options = {}) {
91
- const { params, headers = {}, body, ...fetchOptions } = options;
92
- const url = this.buildUrl(path, params);
93
- const requestHeaders = {
94
- ...this.defaultHeaders
95
- };
96
- const authToken = this.userToken || this.anonKey;
97
- if (authToken) {
98
- requestHeaders["Authorization"] = `Bearer ${authToken}`;
99
- }
100
- let processedBody;
101
- if (body !== void 0) {
102
- if (typeof FormData !== "undefined" && body instanceof FormData) {
103
- processedBody = body;
97
+ if (Array.isArray(body)) return body.map(sanitizeBody);
98
+ if (typeof body === "object") {
99
+ const sanitized = {};
100
+ for (const [key, value] of Object.entries(body)) {
101
+ if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
102
+ sanitized[key] = "***REDACTED***";
104
103
  } else {
105
- if (method !== "GET") {
106
- requestHeaders["Content-Type"] = "application/json;charset=UTF-8";
107
- }
108
- processedBody = JSON.stringify(body);
104
+ sanitized[key] = sanitizeBody(value);
109
105
  }
110
106
  }
111
- Object.assign(requestHeaders, headers);
112
- const response = await this.fetch(url, {
113
- method,
114
- headers: requestHeaders,
115
- body: processedBody,
116
- ...fetchOptions
117
- });
118
- if (response.status === 204) {
119
- return void 0;
120
- }
121
- let data;
122
- const contentType = response.headers.get("content-type");
123
- if (contentType?.includes("json")) {
124
- data = await response.json();
125
- } else {
126
- data = await response.text();
127
- }
128
- if (!response.ok) {
129
- if (data && typeof data === "object" && "error" in data) {
130
- if (!data.statusCode && !data.status) {
131
- data.statusCode = response.status;
132
- }
133
- const error = InsForgeError.fromApiError(data);
134
- Object.keys(data).forEach((key) => {
135
- if (key !== "error" && key !== "message" && key !== "statusCode") {
136
- error[key] = data[key];
137
- }
138
- });
139
- throw error;
140
- }
141
- throw new InsForgeError(
142
- `Request failed: ${response.statusText}`,
143
- response.status,
144
- "REQUEST_FAILED"
145
- );
107
+ return sanitized;
108
+ }
109
+ return body;
110
+ }
111
+ function formatBody(body) {
112
+ if (body === void 0 || body === null) return "";
113
+ if (typeof body === "string") {
114
+ try {
115
+ return JSON.stringify(JSON.parse(body), null, 2);
116
+ } catch {
117
+ return body;
146
118
  }
147
- return data;
148
119
  }
149
- get(path, options) {
150
- return this.request("GET", path, options);
120
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
121
+ return "[FormData]";
151
122
  }
152
- post(path, body, options) {
153
- return this.request("POST", path, { ...options, body });
123
+ try {
124
+ return JSON.stringify(body, null, 2);
125
+ } catch {
126
+ return "[Unserializable body]";
154
127
  }
155
- put(path, body, options) {
156
- return this.request("PUT", path, { ...options, body });
128
+ }
129
+ var Logger = class {
130
+ /**
131
+ * Creates a new Logger instance.
132
+ * @param debug - Set to true to enable console logging, or pass a custom log function
133
+ */
134
+ constructor(debug) {
135
+ if (typeof debug === "function") {
136
+ this.enabled = true;
137
+ this.customLog = debug;
138
+ } else {
139
+ this.enabled = !!debug;
140
+ this.customLog = null;
141
+ }
157
142
  }
158
- patch(path, body, options) {
159
- return this.request("PATCH", path, { ...options, body });
143
+ /**
144
+ * Logs a debug message at the info level.
145
+ * @param message - The message to log
146
+ * @param args - Additional arguments to pass to the log function
147
+ */
148
+ log(message, ...args) {
149
+ if (!this.enabled) return;
150
+ const formatted = `[InsForge Debug] ${message}`;
151
+ if (this.customLog) {
152
+ this.customLog(formatted, ...args);
153
+ } else {
154
+ console.log(formatted, ...args);
155
+ }
160
156
  }
161
- delete(path, options) {
162
- return this.request("DELETE", path, options);
157
+ /**
158
+ * Logs a debug message at the warning level.
159
+ * @param message - The message to log
160
+ * @param args - Additional arguments to pass to the log function
161
+ */
162
+ warn(message, ...args) {
163
+ if (!this.enabled) return;
164
+ const formatted = `[InsForge Debug] ${message}`;
165
+ if (this.customLog) {
166
+ this.customLog(formatted, ...args);
167
+ } else {
168
+ console.warn(formatted, ...args);
169
+ }
163
170
  }
164
- setAuthToken(token) {
165
- this.userToken = token;
171
+ /**
172
+ * Logs a debug message at the error level.
173
+ * @param message - The message to log
174
+ * @param args - Additional arguments to pass to the log function
175
+ */
176
+ error(message, ...args) {
177
+ if (!this.enabled) return;
178
+ const formatted = `[InsForge Debug] ${message}`;
179
+ if (this.customLog) {
180
+ this.customLog(formatted, ...args);
181
+ } else {
182
+ console.error(formatted, ...args);
183
+ }
166
184
  }
167
- getHeaders() {
168
- const headers = { ...this.defaultHeaders };
169
- const authToken = this.userToken || this.anonKey;
170
- if (authToken) {
171
- headers["Authorization"] = `Bearer ${authToken}`;
185
+ /**
186
+ * Logs an outgoing HTTP request with method, URL, headers, and body.
187
+ * Sensitive headers and body fields are automatically redacted.
188
+ * @param method - HTTP method (GET, POST, etc.)
189
+ * @param url - The full request URL
190
+ * @param headers - Request headers (sensitive values will be redacted)
191
+ * @param body - Request body (sensitive fields will be masked)
192
+ */
193
+ logRequest(method, url, headers, body) {
194
+ if (!this.enabled) return;
195
+ const parts = [
196
+ `\u2192 ${method} ${url}`
197
+ ];
198
+ if (headers && Object.keys(headers).length > 0) {
199
+ parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
200
+ }
201
+ const formattedBody = formatBody(sanitizeBody(body));
202
+ if (formattedBody) {
203
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
204
+ parts.push(` Body: ${truncated}`);
205
+ }
206
+ this.log(parts.join("\n"));
207
+ }
208
+ /**
209
+ * Logs an incoming HTTP response with method, URL, status, duration, and body.
210
+ * Error responses (4xx/5xx) are logged at the error level.
211
+ * @param method - HTTP method (GET, POST, etc.)
212
+ * @param url - The full request URL
213
+ * @param status - HTTP response status code
214
+ * @param durationMs - Request duration in milliseconds
215
+ * @param body - Response body (sensitive fields will be masked, large bodies truncated)
216
+ */
217
+ logResponse(method, url, status, durationMs, body) {
218
+ if (!this.enabled) return;
219
+ const parts = [
220
+ `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
221
+ ];
222
+ const formattedBody = formatBody(sanitizeBody(body));
223
+ if (formattedBody) {
224
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
225
+ parts.push(` Body: ${truncated}`);
226
+ }
227
+ if (status >= 400) {
228
+ this.error(parts.join("\n"));
229
+ } else {
230
+ this.log(parts.join("\n"));
172
231
  }
173
- return headers;
174
232
  }
175
233
  };
176
234
 
177
235
  // src/lib/token-manager.ts
178
- var TOKEN_KEY = "insforge-auth-token";
179
- var USER_KEY = "insforge-auth-user";
180
236
  var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
181
237
  function getCsrfToken() {
182
238
  if (typeof document === "undefined") return null;
@@ -196,84 +252,28 @@ function clearCsrfToken() {
196
252
  document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
197
253
  }
198
254
  var TokenManager = class {
199
- constructor(storage) {
255
+ constructor() {
200
256
  // In-memory storage
201
257
  this.accessToken = null;
202
258
  this.user = null;
203
- // Mode: 'memory' (new backend) or 'storage' (legacy backend, default)
204
- this._mode = "storage";
205
- if (storage) {
206
- this.storage = storage;
207
- } else if (typeof window !== "undefined" && window.localStorage) {
208
- this.storage = window.localStorage;
209
- } else {
210
- const store = /* @__PURE__ */ new Map();
211
- this.storage = {
212
- getItem: (key) => store.get(key) || null,
213
- setItem: (key, value) => {
214
- store.set(key, value);
215
- },
216
- removeItem: (key) => {
217
- store.delete(key);
218
- }
219
- };
220
- }
221
- }
222
- /**
223
- * Get current mode
224
- */
225
- get mode() {
226
- return this._mode;
227
- }
228
- /**
229
- * Set mode to memory (new backend with cookies + memory)
230
- */
231
- setMemoryMode() {
232
- if (this._mode === "storage") {
233
- this.storage.removeItem(TOKEN_KEY);
234
- this.storage.removeItem(USER_KEY);
235
- }
236
- this._mode = "memory";
237
- }
238
- /**
239
- * Set mode to storage (legacy backend with localStorage)
240
- * Also loads existing session from localStorage
241
- */
242
- setStorageMode() {
243
- this._mode = "storage";
244
- this.loadFromStorage();
259
+ // Callback for token changes (used by realtime to reconnect with new token)
260
+ this.onTokenChange = null;
245
261
  }
246
262
  /**
247
- * Load session from localStorage
248
- */
249
- loadFromStorage() {
250
- const token = this.storage.getItem(TOKEN_KEY);
251
- const userStr = this.storage.getItem(USER_KEY);
252
- if (token && userStr) {
253
- try {
254
- this.accessToken = token;
255
- this.user = JSON.parse(userStr);
256
- } catch {
257
- this.clearSession();
258
- }
259
- }
260
- }
261
- /**
262
- * Save session (memory always, localStorage only in storage mode)
263
+ * Save session in memory
263
264
  */
264
265
  saveSession(session) {
266
+ const tokenChanged = session.accessToken !== this.accessToken;
265
267
  this.accessToken = session.accessToken;
266
268
  this.user = session.user;
267
- if (this._mode === "storage") {
268
- this.storage.setItem(TOKEN_KEY, session.accessToken);
269
- this.storage.setItem(USER_KEY, JSON.stringify(session.user));
269
+ if (tokenChanged && this.onTokenChange) {
270
+ this.onTokenChange();
270
271
  }
271
272
  }
272
273
  /**
273
274
  * Get current session
274
275
  */
275
276
  getSession() {
276
- this.loadFromStorage();
277
277
  if (!this.accessToken || !this.user) return null;
278
278
  return {
279
279
  accessToken: this.accessToken,
@@ -284,16 +284,16 @@ var TokenManager = class {
284
284
  * Get access token
285
285
  */
286
286
  getAccessToken() {
287
- this.loadFromStorage();
288
287
  return this.accessToken;
289
288
  }
290
289
  /**
291
290
  * Set access token
292
291
  */
293
292
  setAccessToken(token) {
293
+ const tokenChanged = token !== this.accessToken;
294
294
  this.accessToken = token;
295
- if (this._mode === "storage") {
296
- this.storage.setItem(TOKEN_KEY, token);
295
+ if (tokenChanged && this.onTokenChange) {
296
+ this.onTokenChange();
297
297
  }
298
298
  }
299
299
  /**
@@ -307,479 +307,599 @@ var TokenManager = class {
307
307
  */
308
308
  setUser(user) {
309
309
  this.user = user;
310
- if (this._mode === "storage") {
311
- this.storage.setItem(USER_KEY, JSON.stringify(user));
312
- }
313
310
  }
314
311
  /**
315
- * Clear session (both memory and localStorage)
312
+ * Clear in-memory session
316
313
  */
317
314
  clearSession() {
315
+ const hadToken = this.accessToken !== null;
318
316
  this.accessToken = null;
319
317
  this.user = null;
320
- this.storage.removeItem(TOKEN_KEY);
321
- this.storage.removeItem(USER_KEY);
322
- }
323
- /**
324
- * Check if there's a session in localStorage (for legacy detection)
325
- */
326
- hasStoredSession() {
327
- const token = this.storage.getItem(TOKEN_KEY);
328
- return !!token;
318
+ if (hadToken && this.onTokenChange) {
319
+ this.onTokenChange();
320
+ }
329
321
  }
330
322
  };
331
323
 
332
- // src/modules/auth.ts
333
- function isHostedAuthEnvironment() {
334
- if (typeof window === "undefined") {
335
- return false;
336
- }
337
- const { hostname, port, protocol } = window.location;
338
- if (hostname === "localhost" && port === "7130") {
339
- return true;
340
- }
341
- if (protocol === "https:" && hostname.endsWith(".insforge.app")) {
342
- return true;
343
- }
344
- return false;
345
- }
346
- var Auth = class {
347
- constructor(http, tokenManager) {
348
- this.http = http;
349
- this.tokenManager = tokenManager;
350
- this.detectAuthCallback();
351
- }
324
+ // src/lib/http-client.ts
325
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
326
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
327
+ var HttpClient = class {
352
328
  /**
353
- * Automatically detect and handle OAuth callback parameters in the URL
354
- * This runs after initialization to seamlessly complete the OAuth flow
355
- * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544)
329
+ * Creates a new HttpClient instance.
330
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
331
+ * @param tokenManager - Token manager for session persistence.
332
+ * @param logger - Optional logger instance for request/response debugging.
356
333
  */
357
- detectAuthCallback() {
358
- if (typeof window === "undefined") return;
359
- try {
360
- const params = new URLSearchParams(window.location.search);
361
- const accessToken = params.get("access_token");
362
- const userId = params.get("user_id");
363
- const email = params.get("email");
364
- const name = params.get("name");
365
- const csrfToken = params.get("csrf_token");
366
- if (accessToken && userId && email) {
367
- if (csrfToken) {
368
- this.tokenManager.setMemoryMode();
369
- setCsrfToken(csrfToken);
370
- }
371
- const session = {
372
- accessToken,
373
- user: {
374
- id: userId,
375
- email,
376
- profile: { name: name || "" },
377
- metadata: null,
378
- // These fields are not provided by backend OAuth callback
379
- // They'll be populated when calling getCurrentUser()
380
- emailVerified: false,
381
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
382
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
383
- }
384
- };
385
- this.tokenManager.saveSession(session);
386
- this.http.setAuthToken(accessToken);
387
- const url = new URL(window.location.href);
388
- url.searchParams.delete("access_token");
389
- url.searchParams.delete("user_id");
390
- url.searchParams.delete("email");
391
- url.searchParams.delete("name");
392
- url.searchParams.delete("csrf_token");
393
- if (params.has("error")) {
394
- url.searchParams.delete("error");
395
- }
396
- window.history.replaceState({}, document.title, url.toString());
397
- }
398
- } catch (error) {
399
- console.debug("OAuth callback detection skipped:", error);
334
+ constructor(config, tokenManager, logger) {
335
+ this.userToken = null;
336
+ this.autoRefreshToken = true;
337
+ this.isRefreshing = false;
338
+ this.refreshPromise = null;
339
+ this.refreshToken = null;
340
+ this.baseUrl = config.baseUrl || "http://localhost:7130";
341
+ this.autoRefreshToken = config.autoRefreshToken ?? true;
342
+ this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
343
+ this.anonKey = config.anonKey;
344
+ this.defaultHeaders = {
345
+ ...config.headers
346
+ };
347
+ this.tokenManager = tokenManager ?? new TokenManager();
348
+ this.logger = logger || new Logger(false);
349
+ this.timeout = config.timeout ?? 3e4;
350
+ this.retryCount = config.retryCount ?? 3;
351
+ this.retryDelay = config.retryDelay ?? 500;
352
+ if (!this.fetch) {
353
+ throw new Error(
354
+ "Fetch is not available. Please provide a fetch implementation in the config."
355
+ );
400
356
  }
401
357
  }
402
358
  /**
403
- * Sign up a new user
359
+ * Builds a full URL from a path and optional query parameters.
360
+ * Normalizes PostgREST select parameters for proper syntax.
404
361
  */
405
- async signUp(request) {
406
- try {
407
- const response = await this.http.post("/api/auth/users", request);
408
- if (response.accessToken && response.user && !isHostedAuthEnvironment()) {
409
- const session = {
410
- accessToken: response.accessToken,
411
- user: response.user
412
- };
413
- this.tokenManager.saveSession(session);
414
- this.http.setAuthToken(response.accessToken);
415
- if (response.csrfToken) {
416
- setCsrfToken(response.csrfToken);
362
+ buildUrl(path, params) {
363
+ const url = new URL(path, this.baseUrl);
364
+ if (params) {
365
+ Object.entries(params).forEach(([key, value]) => {
366
+ if (key === "select") {
367
+ let normalizedValue = value.replace(/\s+/g, " ").trim();
368
+ normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
369
+ url.searchParams.append(key, normalizedValue);
370
+ } else {
371
+ url.searchParams.append(key, value);
417
372
  }
418
- }
419
- return {
420
- data: response,
421
- error: null
422
- };
423
- } catch (error) {
424
- if (error instanceof InsForgeError) {
425
- return { data: null, error };
426
- }
427
- return {
428
- data: null,
429
- error: new InsForgeError(
430
- error instanceof Error ? error.message : "An unexpected error occurred during sign up",
431
- 500,
432
- "UNEXPECTED_ERROR"
433
- )
434
- };
373
+ });
435
374
  }
375
+ return url.toString();
376
+ }
377
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
378
+ isRetryableStatus(status) {
379
+ return RETRYABLE_STATUS_CODES.has(status);
436
380
  }
437
381
  /**
438
- * Sign in with email and password
382
+ * Computes the delay before the next retry using exponential backoff with jitter.
383
+ * @param attempt - The current retry attempt number (1-based).
384
+ * @returns Delay in milliseconds.
439
385
  */
440
- async signInWithPassword(request) {
441
- try {
442
- const response = await this.http.post("/api/auth/sessions", request);
443
- if (!isHostedAuthEnvironment()) {
444
- const session = {
445
- accessToken: response.accessToken,
446
- user: response.user
447
- };
448
- this.tokenManager.saveSession(session);
449
- this.http.setAuthToken(response.accessToken);
450
- if (response.csrfToken) {
451
- setCsrfToken(response.csrfToken);
452
- }
453
- }
454
- return {
455
- data: response,
456
- error: null
457
- };
458
- } catch (error) {
459
- if (error instanceof InsForgeError) {
460
- return { data: null, error };
461
- }
462
- return {
463
- data: null,
464
- error: new InsForgeError(
465
- "An unexpected error occurred during sign in",
466
- 500,
467
- "UNEXPECTED_ERROR"
468
- )
469
- };
470
- }
386
+ computeRetryDelay(attempt) {
387
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
388
+ const jitter = base * (0.85 + Math.random() * 0.3);
389
+ return Math.round(jitter);
471
390
  }
472
391
  /**
473
- * Sign in with OAuth provider
392
+ * Performs an HTTP request with automatic retry and timeout handling.
393
+ * Retries on network errors and 5xx server errors with exponential backoff.
394
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
395
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
396
+ * @param path - API path relative to the base URL.
397
+ * @param options - Optional request configuration including headers, body, and query params.
398
+ * @returns Parsed response data.
399
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
474
400
  */
475
- async signInWithOAuth(options) {
476
- try {
477
- const { provider, redirectTo, skipBrowserRedirect } = options;
478
- const params = redirectTo ? { redirect_uri: redirectTo } : void 0;
479
- const endpoint = `/api/auth/oauth/${provider}`;
480
- const response = await this.http.get(endpoint, { params });
481
- if (typeof window !== "undefined" && !skipBrowserRedirect) {
482
- window.location.href = response.authUrl;
483
- return { data: {}, error: null };
484
- }
485
- return {
486
- data: {
487
- url: response.authUrl,
488
- provider
489
- },
490
- error: null
491
- };
492
- } catch (error) {
493
- if (error instanceof InsForgeError) {
494
- return { data: {}, error };
401
+ async handleRequest(method, path, options = {}) {
402
+ const {
403
+ params,
404
+ headers = {},
405
+ body,
406
+ signal: callerSignal,
407
+ ...fetchOptions
408
+ } = options;
409
+ const url = this.buildUrl(path, params);
410
+ const startTime = Date.now();
411
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
412
+ const maxAttempts = canRetry ? this.retryCount : 0;
413
+ const requestHeaders = {
414
+ ...this.defaultHeaders
415
+ };
416
+ const authToken = this.userToken || this.anonKey;
417
+ if (authToken) {
418
+ requestHeaders["Authorization"] = `Bearer ${authToken}`;
419
+ }
420
+ let processedBody;
421
+ if (body !== void 0) {
422
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
423
+ processedBody = body;
424
+ } else {
425
+ if (method !== "GET") {
426
+ requestHeaders["Content-Type"] = "application/json;charset=UTF-8";
427
+ }
428
+ processedBody = JSON.stringify(body);
495
429
  }
496
- return {
497
- data: {},
498
- error: new InsForgeError(
499
- "An unexpected error occurred during OAuth initialization",
500
- 500,
501
- "UNEXPECTED_ERROR"
502
- )
503
- };
504
430
  }
505
- }
506
- /**
507
- * Sign out the current user
508
- */
509
- async signOut() {
510
- try {
431
+ if (headers instanceof Headers) {
432
+ headers.forEach((value, key) => {
433
+ requestHeaders[key] = value;
434
+ });
435
+ } else if (Array.isArray(headers)) {
436
+ headers.forEach(([key, value]) => {
437
+ requestHeaders[key] = value;
438
+ });
439
+ } else {
440
+ Object.assign(requestHeaders, headers);
441
+ }
442
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
443
+ let lastError;
444
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
445
+ if (attempt > 0) {
446
+ const delay = this.computeRetryDelay(attempt);
447
+ this.logger.warn(
448
+ `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
449
+ );
450
+ if (callerSignal?.aborted) throw callerSignal.reason;
451
+ await new Promise((resolve, reject) => {
452
+ const onAbort = () => {
453
+ clearTimeout(timer2);
454
+ reject(callerSignal.reason);
455
+ };
456
+ const timer2 = setTimeout(() => {
457
+ if (callerSignal)
458
+ callerSignal.removeEventListener("abort", onAbort);
459
+ resolve();
460
+ }, delay);
461
+ if (callerSignal) {
462
+ callerSignal.addEventListener("abort", onAbort, { once: true });
463
+ }
464
+ });
465
+ }
466
+ let controller;
467
+ let timer;
468
+ if (this.timeout > 0 || callerSignal) {
469
+ controller = new AbortController();
470
+ if (this.timeout > 0) {
471
+ timer = setTimeout(() => controller.abort(), this.timeout);
472
+ }
473
+ if (callerSignal) {
474
+ if (callerSignal.aborted) {
475
+ controller.abort(callerSignal.reason);
476
+ } else {
477
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
478
+ callerSignal.addEventListener("abort", onCallerAbort, {
479
+ once: true
480
+ });
481
+ controller.signal.addEventListener(
482
+ "abort",
483
+ () => {
484
+ callerSignal.removeEventListener("abort", onCallerAbort);
485
+ },
486
+ { once: true }
487
+ );
488
+ }
489
+ }
490
+ }
511
491
  try {
512
- await this.http.post("/api/auth/logout", void 0, { credentials: "include" });
513
- } catch {
492
+ const response = await this.fetch(url, {
493
+ method,
494
+ headers: requestHeaders,
495
+ body: processedBody,
496
+ ...fetchOptions,
497
+ ...controller ? { signal: controller.signal } : {}
498
+ });
499
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
500
+ if (timer !== void 0) clearTimeout(timer);
501
+ await response.body?.cancel();
502
+ lastError = new InsForgeError(
503
+ `Server error: ${response.status} ${response.statusText}`,
504
+ response.status,
505
+ "SERVER_ERROR"
506
+ );
507
+ continue;
508
+ }
509
+ if (response.status === 204) {
510
+ if (timer !== void 0) clearTimeout(timer);
511
+ return void 0;
512
+ }
513
+ let data;
514
+ const contentType = response.headers.get("content-type");
515
+ try {
516
+ if (contentType?.includes("json")) {
517
+ data = await response.json();
518
+ } else {
519
+ data = await response.text();
520
+ }
521
+ } catch (parseErr) {
522
+ if (timer !== void 0) clearTimeout(timer);
523
+ throw new InsForgeError(
524
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
525
+ response.status,
526
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
527
+ );
528
+ }
529
+ if (timer !== void 0) clearTimeout(timer);
530
+ if (!response.ok) {
531
+ this.logger.logResponse(
532
+ method,
533
+ url,
534
+ response.status,
535
+ Date.now() - startTime,
536
+ data
537
+ );
538
+ if (data && typeof data === "object" && "error" in data) {
539
+ if (!data.statusCode && !data.status) {
540
+ data.statusCode = response.status;
541
+ }
542
+ const error = InsForgeError.fromApiError(data);
543
+ Object.keys(data).forEach((key) => {
544
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
545
+ error[key] = data[key];
546
+ }
547
+ });
548
+ throw error;
549
+ }
550
+ throw new InsForgeError(
551
+ `Request failed: ${response.statusText}`,
552
+ response.status,
553
+ "REQUEST_FAILED"
554
+ );
555
+ }
556
+ this.logger.logResponse(
557
+ method,
558
+ url,
559
+ response.status,
560
+ Date.now() - startTime,
561
+ data
562
+ );
563
+ return data;
564
+ } catch (err) {
565
+ if (timer !== void 0) clearTimeout(timer);
566
+ if (err?.name === "AbortError") {
567
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
568
+ throw new InsForgeError(
569
+ `Request timed out after ${this.timeout}ms`,
570
+ 408,
571
+ "REQUEST_TIMEOUT"
572
+ );
573
+ }
574
+ throw err;
575
+ }
576
+ if (err instanceof InsForgeError) {
577
+ throw err;
578
+ }
579
+ if (attempt < maxAttempts) {
580
+ lastError = err;
581
+ continue;
582
+ }
583
+ throw new InsForgeError(
584
+ `Network request failed: ${err?.message || "Unknown error"}`,
585
+ 0,
586
+ "NETWORK_ERROR"
587
+ );
514
588
  }
515
- this.tokenManager.clearSession();
516
- this.http.setAuthToken(null);
517
- clearCsrfToken();
518
- return { error: null };
519
- } catch (error) {
520
- return {
521
- error: new InsForgeError(
522
- "Failed to sign out",
523
- 500,
524
- "SIGNOUT_ERROR"
525
- )
526
- };
527
589
  }
590
+ throw lastError || new InsForgeError(
591
+ "Request failed after all retry attempts",
592
+ 0,
593
+ "NETWORK_ERROR"
594
+ );
528
595
  }
529
- /**
530
- * Get all public authentication configuration (OAuth + Email)
531
- * Returns both OAuth providers and email authentication settings in one request
532
- * This is a public endpoint that doesn't require authentication
533
- *
534
- * @returns Complete public authentication configuration including OAuth providers and email auth settings
535
- *
536
- * @example
537
- * ```ts
538
- * const { data, error } = await insforge.auth.getPublicAuthConfig();
539
- * if (data) {
540
- * console.log(`OAuth providers: ${data.oauth.data.length}`);
541
- * console.log(`Password min length: ${data.email.passwordMinLength}`);
542
- * }
543
- * ```
544
- */
545
- async getPublicAuthConfig() {
596
+ async request(method, path, options = {}) {
546
597
  try {
547
- const response = await this.http.get("/api/auth/public-config");
548
- return {
549
- data: response,
550
- error: null
551
- };
598
+ return await this.handleRequest(method, path, { ...options });
552
599
  } catch (error) {
553
- if (error instanceof InsForgeError) {
554
- return { data: null, error };
600
+ if (error instanceof InsForgeError && error.statusCode === 401 && error.error === "INVALID_TOKEN" && this.autoRefreshToken) {
601
+ try {
602
+ const newTokenData = await this.handleTokenRefresh();
603
+ this.setAuthToken(newTokenData.accessToken);
604
+ this.tokenManager.saveSession(newTokenData);
605
+ if (newTokenData.csrfToken) {
606
+ setCsrfToken(newTokenData.csrfToken);
607
+ }
608
+ if (newTokenData.refreshToken) {
609
+ this.setRefreshToken(newTokenData.refreshToken);
610
+ }
611
+ return await this.handleRequest(method, path, { ...options });
612
+ } catch (error2) {
613
+ this.tokenManager.clearSession();
614
+ this.userToken = null;
615
+ this.refreshToken = null;
616
+ clearCsrfToken();
617
+ throw error2;
618
+ }
555
619
  }
556
- return {
557
- data: null,
558
- error: new InsForgeError(
559
- "An unexpected error occurred while fetching public authentication configuration",
560
- 500,
561
- "UNEXPECTED_ERROR"
562
- )
563
- };
620
+ throw error;
564
621
  }
565
622
  }
566
- /**
567
- * Get the current user with full profile information
568
- * Returns both auth info (id, email, role) and profile data (dynamic fields from users table)
569
- */
570
- async getCurrentUser() {
571
- try {
572
- const user = this.tokenManager.getUser();
573
- if (user) {
574
- return { data: { user }, error: null };
575
- }
576
- const accessToken = this.tokenManager.getAccessToken();
577
- if (!accessToken) {
578
- return { data: null, error: null };
579
- }
580
- this.http.setAuthToken(accessToken);
581
- const authResponse = await this.http.get("/api/auth/sessions/current");
582
- return {
583
- data: {
584
- user: authResponse.user
585
- },
586
- error: null
587
- };
588
- } catch (error) {
589
- if (error instanceof InsForgeError && error.statusCode === 401) {
590
- await this.signOut();
591
- return { data: null, error: null };
592
- }
593
- if (error instanceof InsForgeError) {
594
- return { data: null, error };
595
- }
596
- return {
597
- data: null,
598
- error: new InsForgeError(
599
- "An unexpected error occurred while fetching user",
600
- 500,
601
- "UNEXPECTED_ERROR"
602
- )
603
- };
623
+ /** Performs a GET request. */
624
+ get(path, options) {
625
+ return this.request("GET", path, options);
626
+ }
627
+ /** Performs a POST request with an optional JSON body. */
628
+ post(path, body, options) {
629
+ return this.request("POST", path, { ...options, body });
630
+ }
631
+ /** Performs a PUT request with an optional JSON body. */
632
+ put(path, body, options) {
633
+ return this.request("PUT", path, { ...options, body });
634
+ }
635
+ /** Performs a PATCH request with an optional JSON body. */
636
+ patch(path, body, options) {
637
+ return this.request("PATCH", path, { ...options, body });
638
+ }
639
+ /** Performs a DELETE request. */
640
+ delete(path, options) {
641
+ return this.request("DELETE", path, options);
642
+ }
643
+ /** Sets or clears the user authentication token for subsequent requests. */
644
+ setAuthToken(token) {
645
+ this.userToken = token;
646
+ }
647
+ setRefreshToken(token) {
648
+ this.refreshToken = token;
649
+ }
650
+ /** Returns the current default headers including the authorization header if set. */
651
+ getHeaders() {
652
+ const headers = { ...this.defaultHeaders };
653
+ const authToken = this.userToken || this.anonKey;
654
+ if (authToken) {
655
+ headers["Authorization"] = `Bearer ${authToken}`;
656
+ }
657
+ return headers;
658
+ }
659
+ async handleTokenRefresh() {
660
+ if (this.isRefreshing) {
661
+ return this.refreshPromise;
604
662
  }
663
+ this.isRefreshing = true;
664
+ this.refreshPromise = (async () => {
665
+ try {
666
+ const csrfToken = getCsrfToken();
667
+ const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
668
+ const response = await this.handleRequest(
669
+ "POST",
670
+ "/api/auth/sessions/current",
671
+ {
672
+ body,
673
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
674
+ credentials: "include"
675
+ }
676
+ );
677
+ return response;
678
+ } finally {
679
+ this.isRefreshing = false;
680
+ this.refreshPromise = null;
681
+ }
682
+ })();
683
+ return this.refreshPromise;
684
+ }
685
+ };
686
+
687
+ // src/modules/auth/helpers.ts
688
+ var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
689
+ function base64UrlEncode(buffer) {
690
+ const base64 = btoa(String.fromCharCode(...buffer));
691
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
692
+ }
693
+ function generateCodeVerifier() {
694
+ const array = new Uint8Array(32);
695
+ crypto.getRandomValues(array);
696
+ return base64UrlEncode(array);
697
+ }
698
+ async function generateCodeChallenge(verifier) {
699
+ const encoder = new TextEncoder();
700
+ const data = encoder.encode(verifier);
701
+ const hash = await crypto.subtle.digest("SHA-256", data);
702
+ return base64UrlEncode(new Uint8Array(hash));
703
+ }
704
+ function storePkceVerifier(verifier) {
705
+ if (typeof sessionStorage !== "undefined") {
706
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
707
+ }
708
+ }
709
+ function retrievePkceVerifier() {
710
+ if (typeof sessionStorage === "undefined") {
711
+ return null;
712
+ }
713
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
714
+ if (verifier) {
715
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
716
+ }
717
+ return verifier;
718
+ }
719
+ function wrapError(error, fallbackMessage) {
720
+ if (error instanceof InsForgeError) {
721
+ return { data: null, error };
722
+ }
723
+ return {
724
+ data: null,
725
+ error: new InsForgeError(
726
+ error instanceof Error ? error.message : fallbackMessage,
727
+ 500,
728
+ "UNEXPECTED_ERROR"
729
+ )
730
+ };
731
+ }
732
+ function cleanUrlParams(...params) {
733
+ if (typeof window === "undefined") {
734
+ return;
735
+ }
736
+ const url = new URL(window.location.href);
737
+ params.forEach((p) => url.searchParams.delete(p));
738
+ window.history.replaceState({}, document.title, url.toString());
739
+ }
740
+
741
+ // src/modules/auth/auth.ts
742
+ var import_shared_schemas = require("@insforge/shared-schemas");
743
+ var Auth = class {
744
+ constructor(http, tokenManager, options = {}) {
745
+ this.http = http;
746
+ this.tokenManager = tokenManager;
747
+ this.options = options;
748
+ this.authCallbackHandled = this.detectAuthCallback();
749
+ }
750
+ isServerMode() {
751
+ return !!this.options.isServerMode;
605
752
  }
606
753
  /**
607
- * Get any user's profile by ID
608
- * Returns profile information from the users table
754
+ * Save session from API response
755
+ * Handles token storage, CSRF token, and HTTP auth header
609
756
  */
610
- async getProfile(userId) {
611
- try {
612
- const response = await this.http.get(`/api/auth/profiles/${userId}`);
613
- return {
614
- data: response,
615
- error: null
616
- };
617
- } catch (error) {
618
- if (error instanceof InsForgeError) {
619
- return { data: null, error };
620
- }
621
- return {
622
- data: null,
623
- error: new InsForgeError(
624
- "An unexpected error occurred while fetching user profile",
625
- 500,
626
- "UNEXPECTED_ERROR"
627
- )
628
- };
757
+ saveSessionFromResponse(response) {
758
+ if (!response.accessToken || !response.user) {
759
+ return false;
760
+ }
761
+ const session = {
762
+ accessToken: response.accessToken,
763
+ user: response.user
764
+ };
765
+ if (!this.isServerMode() && response.csrfToken) {
766
+ setCsrfToken(response.csrfToken);
767
+ }
768
+ if (!this.isServerMode()) {
769
+ this.tokenManager.saveSession(session);
629
770
  }
771
+ this.http.setAuthToken(response.accessToken);
772
+ this.http.setRefreshToken(response.refreshToken ?? null);
773
+ return true;
630
774
  }
775
+ // ============================================================================
776
+ // OAuth Callback Detection (runs on initialization)
777
+ // ============================================================================
631
778
  /**
632
- * Get the current session (only session data, no API call)
633
- * Returns the stored JWT token and basic user info from local storage
779
+ * Detect and handle OAuth callback parameters in URL
780
+ * Supports PKCE flow (insforge_code)
634
781
  */
635
- async getCurrentSession() {
782
+ async detectAuthCallback() {
783
+ if (this.isServerMode() || typeof window === "undefined") return;
636
784
  try {
637
- const session = this.tokenManager.getSession();
638
- if (session) {
639
- this.http.setAuthToken(session.accessToken);
640
- return { data: { session }, error: null };
785
+ const params = new URLSearchParams(window.location.search);
786
+ const error = params.get("error");
787
+ if (error) {
788
+ cleanUrlParams("error");
789
+ console.debug("OAuth callback error:", error);
790
+ return;
641
791
  }
642
- if (typeof window !== "undefined") {
643
- try {
644
- const csrfToken = getCsrfToken();
645
- const response = await this.http.post(
646
- "/api/auth/refresh",
647
- void 0,
648
- {
649
- headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
650
- credentials: "include"
651
- }
652
- );
653
- if (response.accessToken) {
654
- this.tokenManager.setMemoryMode();
655
- this.tokenManager.setAccessToken(response.accessToken);
656
- this.http.setAuthToken(response.accessToken);
657
- if (response.user) {
658
- this.tokenManager.setUser(response.user);
659
- }
660
- if (response.csrfToken) {
661
- setCsrfToken(response.csrfToken);
662
- }
663
- return {
664
- data: { session: this.tokenManager.getSession() },
665
- error: null
666
- };
667
- }
668
- } catch (error) {
669
- if (error instanceof InsForgeError) {
670
- if (error.statusCode === 404) {
671
- this.tokenManager.setStorageMode();
672
- const session2 = this.tokenManager.getSession();
673
- if (session2) {
674
- return { data: { session: session2 }, error: null };
675
- }
676
- return { data: { session: null }, error: null };
677
- }
678
- return { data: { session: null }, error };
679
- }
792
+ const code = params.get("insforge_code");
793
+ if (code) {
794
+ cleanUrlParams("insforge_code");
795
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
796
+ if (exchangeError) {
797
+ console.debug("OAuth code exchange failed:", exchangeError.message);
680
798
  }
799
+ return;
681
800
  }
682
- return { data: { session: null }, error: null };
683
801
  } catch (error) {
684
- if (error instanceof InsForgeError) {
685
- return { data: { session: null }, error };
686
- }
687
- return {
688
- data: { session: null },
689
- error: new InsForgeError(
690
- "An unexpected error occurred while getting session",
691
- 500,
692
- "UNEXPECTED_ERROR"
693
- )
694
- };
802
+ console.debug("OAuth callback detection skipped:", error);
695
803
  }
696
804
  }
697
- /**
698
- * Set/Update the current user's profile
699
- * Updates profile information in the users table (supports any dynamic fields)
700
- * Requires authentication
701
- */
702
- async setProfile(profile) {
805
+ // ============================================================================
806
+ // Sign Up / Sign In / Sign Out
807
+ // ============================================================================
808
+ async signUp(request) {
703
809
  try {
704
- const response = await this.http.patch(
705
- "/api/auth/profiles/current",
706
- { profile }
810
+ const response = await this.http.post(
811
+ this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
812
+ request,
813
+ { credentials: "include" }
707
814
  );
708
- return {
709
- data: response,
710
- error: null
711
- };
712
- } catch (error) {
713
- if (error instanceof InsForgeError) {
714
- return { data: null, error };
815
+ if (response.accessToken && response.user) {
816
+ this.saveSessionFromResponse(response);
715
817
  }
716
- return {
717
- data: null,
718
- error: new InsForgeError(
719
- "An unexpected error occurred while updating user profile",
720
- 500,
721
- "UNEXPECTED_ERROR"
722
- )
723
- };
818
+ if (response.refreshToken) {
819
+ this.http.setRefreshToken(response.refreshToken);
820
+ }
821
+ return { data: response, error: null };
822
+ } catch (error) {
823
+ return wrapError(error, "An unexpected error occurred during sign up");
724
824
  }
725
825
  }
726
- /**
727
- * Send email verification (code or link based on config)
728
- *
729
- * Send email verification using the method configured in auth settings (verifyEmailMethod).
730
- * When method is 'code', sends a 6-digit numeric code. When method is 'link', sends a magic link.
731
- * Prevents user enumeration by returning success even if email doesn't exist.
732
- */
733
- async sendVerificationEmail(request) {
826
+ async signInWithPassword(request) {
734
827
  try {
735
828
  const response = await this.http.post(
736
- "/api/auth/email/send-verification",
737
- request
829
+ this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
830
+ request,
831
+ { credentials: "include" }
738
832
  );
739
- return {
740
- data: response,
741
- error: null
742
- };
833
+ this.saveSessionFromResponse(response);
834
+ if (response.refreshToken) {
835
+ this.http.setRefreshToken(response.refreshToken);
836
+ }
837
+ return { data: response, error: null };
743
838
  } catch (error) {
744
- if (error instanceof InsForgeError) {
745
- return { data: null, error };
839
+ return wrapError(error, "An unexpected error occurred during sign in");
840
+ }
841
+ }
842
+ async signOut() {
843
+ try {
844
+ try {
845
+ await this.http.post(
846
+ this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
847
+ void 0,
848
+ { credentials: "include" }
849
+ );
850
+ } catch {
746
851
  }
852
+ this.tokenManager.clearSession();
853
+ this.http.setAuthToken(null);
854
+ this.http.setRefreshToken(null);
855
+ if (!this.isServerMode()) {
856
+ clearCsrfToken();
857
+ }
858
+ return { error: null };
859
+ } catch {
747
860
  return {
748
- data: null,
749
- error: new InsForgeError(
750
- "An unexpected error occurred while sending verification code",
751
- 500,
752
- "UNEXPECTED_ERROR"
753
- )
861
+ error: new InsForgeError("Failed to sign out", 500, "SIGNOUT_ERROR")
754
862
  };
755
863
  }
756
864
  }
865
+ // ============================================================================
866
+ // OAuth Authentication
867
+ // ============================================================================
757
868
  /**
758
- * Send password reset (code or link based on config)
759
- *
760
- * Send password reset email using the method configured in auth settings (resetPasswordMethod).
761
- * When method is 'code', sends a 6-digit numeric code for two-step flow.
762
- * When method is 'link', sends a magic link.
763
- * Prevents user enumeration by returning success even if email doesn't exist.
869
+ * Sign in with OAuth provider using PKCE flow
764
870
  */
765
- async sendResetPasswordEmail(request) {
871
+ async signInWithOAuth(options) {
766
872
  try {
767
- const response = await this.http.post(
768
- "/api/auth/email/send-reset-password",
769
- request
873
+ const { provider, redirectTo, skipBrowserRedirect } = options;
874
+ const providerKey = encodeURIComponent(provider.toLowerCase());
875
+ const codeVerifier = generateCodeVerifier();
876
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
877
+ storePkceVerifier(codeVerifier);
878
+ const params = { code_challenge: codeChallenge };
879
+ if (redirectTo) params.redirect_uri = redirectTo;
880
+ const isBuiltInProvider = import_shared_schemas.oAuthProvidersSchema.options.includes(
881
+ providerKey
770
882
  );
883
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
884
+ const response = await this.http.get(oauthPath, {
885
+ params
886
+ });
887
+ if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
888
+ window.location.href = response.authUrl;
889
+ return { data: {}, error: null };
890
+ }
771
891
  return {
772
- data: response,
892
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
773
893
  error: null
774
894
  };
775
895
  } catch (error) {
776
896
  if (error instanceof InsForgeError) {
777
- return { data: null, error };
897
+ return { data: {}, error };
778
898
  }
779
899
  return {
780
- data: null,
900
+ data: {},
781
901
  error: new InsForgeError(
782
- "An unexpected error occurred while sending password reset code",
902
+ "An unexpected error occurred during OAuth initialization",
783
903
  500,
784
904
  "UNEXPECTED_ERROR"
785
905
  )
@@ -787,123 +907,292 @@ var Auth = class {
787
907
  }
788
908
  }
789
909
  /**
790
- * Exchange reset password code for reset token
791
- *
792
- * Step 1 of two-step password reset flow (only used when resetPasswordMethod is 'code'):
793
- * 1. Verify the 6-digit code sent to user's email
794
- * 2. Return a reset token that can be used to actually reset the password
795
- *
796
- * This endpoint is not used when resetPasswordMethod is 'link' (magic link flow is direct).
910
+ * Exchange OAuth authorization code for tokens (PKCE flow)
911
+ * Called automatically on initialization when insforge_code is in URL
797
912
  */
798
- async exchangeResetPasswordToken(request) {
913
+ async exchangeOAuthCode(code, codeVerifier) {
799
914
  try {
915
+ const verifier = codeVerifier ?? retrievePkceVerifier();
916
+ if (!verifier) {
917
+ return {
918
+ data: null,
919
+ error: new InsForgeError(
920
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
921
+ 400,
922
+ "PKCE_VERIFIER_MISSING"
923
+ )
924
+ };
925
+ }
926
+ const request = {
927
+ code,
928
+ code_verifier: verifier
929
+ };
800
930
  const response = await this.http.post(
801
- "/api/auth/email/exchange-reset-password-token",
802
- request
931
+ this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
932
+ request,
933
+ { credentials: "include" }
803
934
  );
935
+ this.saveSessionFromResponse(response);
804
936
  return {
805
937
  data: response,
806
938
  error: null
807
939
  };
808
940
  } catch (error) {
809
- if (error instanceof InsForgeError) {
810
- return { data: null, error };
811
- }
812
- return {
813
- data: null,
814
- error: new InsForgeError(
815
- "An unexpected error occurred while verifying reset code",
816
- 500,
817
- "UNEXPECTED_ERROR"
818
- )
819
- };
941
+ return wrapError(
942
+ error,
943
+ "An unexpected error occurred during OAuth code exchange"
944
+ );
820
945
  }
821
946
  }
822
947
  /**
823
- * Reset password with token
824
- *
825
- * Reset user password with a token. The token can be:
826
- * - Magic link token (64-character hex token from send-reset-password when method is 'link')
827
- * - Reset token (from exchange-reset-password-token after code verification when method is 'code')
828
- *
829
- * Both token types use RESET_PASSWORD purpose and are verified the same way.
948
+ * Sign in with an ID token from a native SDK (Google One Tap, etc.)
949
+ * Use this for native mobile apps or Google One Tap on web.
830
950
  *
831
- * Flow summary:
832
- * - Code method: send-reset-password exchange-reset-password-token reset-password (with resetToken)
833
- * - Link method: send-reset-password → reset-password (with link token directly)
951
+ * @param credentials.provider - The identity provider (currently only 'google' is supported)
952
+ * @param credentials.token - The ID token from the native SDK
834
953
  */
835
- async resetPassword(request) {
954
+ async signInWithIdToken(credentials) {
836
955
  try {
956
+ const { provider, token } = credentials;
837
957
  const response = await this.http.post(
838
- "/api/auth/email/reset-password",
839
- request
958
+ "/api/auth/id-token?client_type=mobile",
959
+ { provider, token },
960
+ { credentials: "include" }
840
961
  );
962
+ this.saveSessionFromResponse(response);
963
+ if (response.refreshToken) {
964
+ this.http.setRefreshToken(response.refreshToken);
965
+ }
841
966
  return {
842
967
  data: response,
843
968
  error: null
844
969
  };
845
970
  } catch (error) {
846
- if (error instanceof InsForgeError) {
847
- return { data: null, error };
848
- }
849
- return {
850
- data: null,
851
- error: new InsForgeError(
852
- "An unexpected error occurred while resetting password",
853
- 500,
854
- "UNEXPECTED_ERROR"
855
- )
856
- };
971
+ return wrapError(
972
+ error,
973
+ "An unexpected error occurred during ID token sign in"
974
+ );
857
975
  }
858
976
  }
977
+ // ============================================================================
978
+ // Session Management
979
+ // ============================================================================
859
980
  /**
860
- * Verify email with code or link
981
+ * Refresh the current auth session.
861
982
  *
862
- * Verify email address using the method configured in auth settings (verifyEmailMethod):
863
- * - Code verification: Provide both `email` and `otp` (6-digit numeric code)
864
- * - Link verification: Provide only `otp` (64-character hex token from magic link)
983
+ * Browser mode:
984
+ * - Uses httpOnly refresh cookie and optional CSRF header.
865
985
  *
866
- * Successfully verified users will receive a session token.
867
- *
868
- * The email verification link sent to users always points to the backend API endpoint.
869
- * If `verifyEmailRedirectTo` is configured, the backend will redirect to that URL after successful verification.
870
- * Otherwise, a default success page is displayed.
986
+ * Server mode (`isServerMode: true`):
987
+ * - Uses mobile auth flow and requires `refreshToken` in request body.
871
988
  */
872
- async verifyEmail(request) {
989
+ async refreshSession(options) {
873
990
  try {
991
+ if (this.isServerMode() && !options?.refreshToken) {
992
+ return {
993
+ data: null,
994
+ error: new InsForgeError(
995
+ "refreshToken is required when refreshing session in server mode",
996
+ 400,
997
+ "REFRESH_TOKEN_REQUIRED"
998
+ )
999
+ };
1000
+ }
1001
+ const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
874
1002
  const response = await this.http.post(
875
- "/api/auth/email/verify",
876
- request
1003
+ this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
1004
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
1005
+ {
1006
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
1007
+ credentials: "include"
1008
+ }
877
1009
  );
878
- if (!isHostedAuthEnvironment()) {
879
- const session = {
880
- accessToken: response.accessToken,
881
- user: response.user
882
- };
883
- this.tokenManager.saveSession(session);
884
- this.http.setAuthToken(response.accessToken);
885
- if (response.csrfToken) {
886
- setCsrfToken(response.csrfToken);
1010
+ if (response.accessToken) {
1011
+ this.saveSessionFromResponse(response);
1012
+ }
1013
+ return { data: response, error: null };
1014
+ } catch (error) {
1015
+ return wrapError(
1016
+ error,
1017
+ "An unexpected error occurred during session refresh"
1018
+ );
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Get current user, automatically waits for pending OAuth callback
1023
+ */
1024
+ async getCurrentUser() {
1025
+ await this.authCallbackHandled;
1026
+ try {
1027
+ if (this.isServerMode()) {
1028
+ const accessToken = this.tokenManager.getAccessToken();
1029
+ if (!accessToken) return { data: { user: null }, error: null };
1030
+ this.http.setAuthToken(accessToken);
1031
+ const response = await this.http.get(
1032
+ "/api/auth/sessions/current"
1033
+ );
1034
+ const user = response.user ?? null;
1035
+ return { data: { user }, error: null };
1036
+ }
1037
+ const session = this.tokenManager.getSession();
1038
+ if (session) {
1039
+ this.http.setAuthToken(session.accessToken);
1040
+ return { data: { user: session.user }, error: null };
1041
+ }
1042
+ if (typeof window !== "undefined") {
1043
+ const { data: refreshed, error: refreshError } = await this.refreshSession();
1044
+ if (refreshError) {
1045
+ return { data: { user: null }, error: refreshError };
1046
+ }
1047
+ if (refreshed?.accessToken) {
1048
+ return { data: { user: refreshed.user ?? null }, error: null };
887
1049
  }
888
1050
  }
889
- return {
890
- data: response,
891
- error: null
892
- };
1051
+ return { data: { user: null }, error: null };
893
1052
  } catch (error) {
894
1053
  if (error instanceof InsForgeError) {
895
- return { data: null, error };
1054
+ return { data: { user: null }, error };
896
1055
  }
897
1056
  return {
898
- data: null,
1057
+ data: { user: null },
899
1058
  error: new InsForgeError(
900
- "An unexpected error occurred while verifying email",
1059
+ "An unexpected error occurred while getting user",
901
1060
  500,
902
1061
  "UNEXPECTED_ERROR"
903
1062
  )
904
1063
  };
905
1064
  }
906
1065
  }
1066
+ // ============================================================================
1067
+ // Profile Management
1068
+ // ============================================================================
1069
+ async getProfile(userId) {
1070
+ try {
1071
+ const response = await this.http.get(
1072
+ `/api/auth/profiles/${userId}`
1073
+ );
1074
+ return { data: response, error: null };
1075
+ } catch (error) {
1076
+ return wrapError(
1077
+ error,
1078
+ "An unexpected error occurred while fetching user profile"
1079
+ );
1080
+ }
1081
+ }
1082
+ async setProfile(profile) {
1083
+ try {
1084
+ const response = await this.http.patch(
1085
+ "/api/auth/profiles/current",
1086
+ {
1087
+ profile
1088
+ }
1089
+ );
1090
+ const currentUser = this.tokenManager.getUser();
1091
+ if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1092
+ this.tokenManager.setUser({
1093
+ ...currentUser,
1094
+ profile: response.profile
1095
+ });
1096
+ }
1097
+ return { data: response, error: null };
1098
+ } catch (error) {
1099
+ return wrapError(
1100
+ error,
1101
+ "An unexpected error occurred while updating user profile"
1102
+ );
1103
+ }
1104
+ }
1105
+ // ============================================================================
1106
+ // Email Verification
1107
+ // ============================================================================
1108
+ async resendVerificationEmail(request) {
1109
+ try {
1110
+ const response = await this.http.post("/api/auth/email/send-verification", request);
1111
+ return { data: response, error: null };
1112
+ } catch (error) {
1113
+ return wrapError(
1114
+ error,
1115
+ "An unexpected error occurred while sending verification email"
1116
+ );
1117
+ }
1118
+ }
1119
+ async verifyEmail(request) {
1120
+ try {
1121
+ const response = await this.http.post(
1122
+ this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1123
+ request,
1124
+ { credentials: "include" }
1125
+ );
1126
+ this.saveSessionFromResponse(response);
1127
+ if (response.refreshToken) {
1128
+ this.http.setRefreshToken(response.refreshToken);
1129
+ }
1130
+ return { data: response, error: null };
1131
+ } catch (error) {
1132
+ return wrapError(
1133
+ error,
1134
+ "An unexpected error occurred while verifying email"
1135
+ );
1136
+ }
1137
+ }
1138
+ // ============================================================================
1139
+ // Password Reset
1140
+ // ============================================================================
1141
+ async sendResetPasswordEmail(request) {
1142
+ try {
1143
+ const response = await this.http.post("/api/auth/email/send-reset-password", request);
1144
+ return { data: response, error: null };
1145
+ } catch (error) {
1146
+ return wrapError(
1147
+ error,
1148
+ "An unexpected error occurred while sending password reset email"
1149
+ );
1150
+ }
1151
+ }
1152
+ async exchangeResetPasswordToken(request) {
1153
+ try {
1154
+ const response = await this.http.post(
1155
+ "/api/auth/email/exchange-reset-password-token",
1156
+ request
1157
+ );
1158
+ return { data: response, error: null };
1159
+ } catch (error) {
1160
+ return wrapError(
1161
+ error,
1162
+ "An unexpected error occurred while verifying reset code"
1163
+ );
1164
+ }
1165
+ }
1166
+ async resetPassword(request) {
1167
+ try {
1168
+ const response = await this.http.post(
1169
+ "/api/auth/email/reset-password",
1170
+ request
1171
+ );
1172
+ return { data: response, error: null };
1173
+ } catch (error) {
1174
+ return wrapError(
1175
+ error,
1176
+ "An unexpected error occurred while resetting password"
1177
+ );
1178
+ }
1179
+ }
1180
+ // ============================================================================
1181
+ // Configuration
1182
+ // ============================================================================
1183
+ async getPublicAuthConfig() {
1184
+ try {
1185
+ const response = await this.http.get(
1186
+ "/api/auth/public-config"
1187
+ );
1188
+ return { data: response, error: null };
1189
+ } catch (error) {
1190
+ return wrapError(
1191
+ error,
1192
+ "An unexpected error occurred while fetching auth configuration"
1193
+ );
1194
+ }
1195
+ }
907
1196
  };
908
1197
 
909
1198
  // src/modules/database-postgrest.ts
@@ -912,8 +1201,10 @@ function createInsForgePostgrestFetch(httpClient, tokenManager) {
912
1201
  return async (input, init) => {
913
1202
  const url = typeof input === "string" ? input : input.toString();
914
1203
  const urlObj = new URL(url);
915
- const tableName = urlObj.pathname.slice(1);
916
- const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
1204
+ const pathname = urlObj.pathname.slice(1);
1205
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1206
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1207
+ const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
917
1208
  const token = tokenManager.getAccessToken();
918
1209
  const httpHeaders = httpClient.getHeaders();
919
1210
  const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
@@ -973,6 +1264,25 @@ var Database = class {
973
1264
  from(table) {
974
1265
  return this.postgrest.from(table);
975
1266
  }
1267
+ /**
1268
+ * Call a PostgreSQL function (RPC)
1269
+ *
1270
+ * @example
1271
+ * // Call a function with parameters
1272
+ * const { data, error } = await client.database
1273
+ * .rpc('get_user_stats', { user_id: 123 });
1274
+ *
1275
+ * // Call a function with no parameters
1276
+ * const { data, error } = await client.database
1277
+ * .rpc('get_all_active_users');
1278
+ *
1279
+ * // With options (head, count, get)
1280
+ * const { data, count } = await client.database
1281
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1282
+ */
1283
+ rpc(fn, args, options) {
1284
+ return this.postgrest.rpc(fn, args, options);
1285
+ }
976
1286
  };
977
1287
 
978
1288
  // src/modules/storage.ts
@@ -1257,6 +1567,7 @@ var AI = class {
1257
1567
  this.http = http;
1258
1568
  this.chat = new Chat(http);
1259
1569
  this.images = new Images(http);
1570
+ this.embeddings = new Embeddings(http);
1260
1571
  }
1261
1572
  };
1262
1573
  var Chat = class {
@@ -1280,16 +1591,46 @@ var ChatCompletions = class {
1280
1591
  * });
1281
1592
  * console.log(completion.choices[0].message.content);
1282
1593
  *
1283
- * // With images
1594
+ * // With images (OpenAI-compatible format)
1284
1595
  * const response = await client.ai.chat.completions.create({
1285
1596
  * model: 'gpt-4-vision',
1286
1597
  * messages: [{
1287
1598
  * role: 'user',
1288
- * content: 'What is in this image?',
1289
- * images: [{ url: 'https://example.com/image.jpg' }]
1599
+ * content: [
1600
+ * { type: 'text', text: 'What is in this image?' },
1601
+ * { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
1602
+ * ]
1290
1603
  * }]
1291
1604
  * });
1292
1605
  *
1606
+ * // With PDF files
1607
+ * const pdfResponse = await client.ai.chat.completions.create({
1608
+ * model: 'anthropic/claude-3.5-sonnet',
1609
+ * messages: [{
1610
+ * role: 'user',
1611
+ * content: [
1612
+ * { type: 'text', text: 'Summarize this document' },
1613
+ * { type: 'file', file: { filename: 'doc.pdf', file_data: 'https://example.com/doc.pdf' } }
1614
+ * ]
1615
+ * }],
1616
+ * fileParser: { enabled: true, pdf: { engine: 'mistral-ocr' } }
1617
+ * });
1618
+ *
1619
+ * // With web search
1620
+ * const searchResponse = await client.ai.chat.completions.create({
1621
+ * model: 'openai/gpt-4',
1622
+ * messages: [{ role: 'user', content: 'What are the latest news about AI?' }],
1623
+ * webSearch: { enabled: true, maxResults: 5 }
1624
+ * });
1625
+ * // Access citations from response.choices[0].message.annotations
1626
+ *
1627
+ * // With thinking/reasoning mode (Anthropic models)
1628
+ * const thinkingResponse = await client.ai.chat.completions.create({
1629
+ * model: 'anthropic/claude-3.5-sonnet',
1630
+ * messages: [{ role: 'user', content: 'Solve this complex math problem...' }],
1631
+ * thinking: true
1632
+ * });
1633
+ *
1293
1634
  * // Streaming - returns async iterable
1294
1635
  * const stream = await client.ai.chat.completions.create({
1295
1636
  * model: 'gpt-4',
@@ -1311,7 +1652,15 @@ var ChatCompletions = class {
1311
1652
  temperature: params.temperature,
1312
1653
  maxTokens: params.maxTokens,
1313
1654
  topP: params.topP,
1314
- stream: params.stream
1655
+ stream: params.stream,
1656
+ // New plugin options
1657
+ webSearch: params.webSearch,
1658
+ fileParser: params.fileParser,
1659
+ thinking: params.thinking,
1660
+ // Tool calling options
1661
+ tools: params.tools,
1662
+ toolChoice: params.toolChoice,
1663
+ parallelToolCalls: params.parallelToolCalls
1315
1664
  };
1316
1665
  if (params.stream) {
1317
1666
  const headers = this.http.getHeaders();
@@ -1345,9 +1694,13 @@ var ChatCompletions = class {
1345
1694
  index: 0,
1346
1695
  message: {
1347
1696
  role: "assistant",
1348
- content
1697
+ content,
1698
+ // Include tool_calls if present (from tool calling)
1699
+ ...response.tool_calls?.length && { tool_calls: response.tool_calls },
1700
+ // Include annotations if present (from web search or file parsing)
1701
+ ...response.annotations?.length && { annotations: response.annotations }
1349
1702
  },
1350
- finish_reason: "stop"
1703
+ finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
1351
1704
  }
1352
1705
  ],
1353
1706
  usage: response.metadata?.usage || {
@@ -1389,7 +1742,24 @@ var ChatCompletions = class {
1389
1742
  delta: {
1390
1743
  content: data.chunk || data.content
1391
1744
  },
1392
- finish_reason: data.done ? "stop" : null
1745
+ finish_reason: null
1746
+ }
1747
+ ]
1748
+ };
1749
+ }
1750
+ if (data.tool_calls?.length) {
1751
+ yield {
1752
+ id: `chatcmpl-${Date.now()}`,
1753
+ object: "chat.completion.chunk",
1754
+ created: Math.floor(Date.now() / 1e3),
1755
+ model,
1756
+ choices: [
1757
+ {
1758
+ index: 0,
1759
+ delta: {
1760
+ tool_calls: data.tool_calls
1761
+ },
1762
+ finish_reason: "tool_calls"
1393
1763
  }
1394
1764
  ]
1395
1765
  };
@@ -1410,6 +1780,65 @@ var ChatCompletions = class {
1410
1780
  }
1411
1781
  }
1412
1782
  };
1783
+ var Embeddings = class {
1784
+ constructor(http) {
1785
+ this.http = http;
1786
+ }
1787
+ /**
1788
+ * Create embeddings for text input - OpenAI-like response format
1789
+ *
1790
+ * @example
1791
+ * ```typescript
1792
+ * // Single text input
1793
+ * const response = await client.ai.embeddings.create({
1794
+ * model: 'openai/text-embedding-3-small',
1795
+ * input: 'Hello world'
1796
+ * });
1797
+ * console.log(response.data[0].embedding); // number[]
1798
+ *
1799
+ * // Multiple text inputs
1800
+ * const response = await client.ai.embeddings.create({
1801
+ * model: 'openai/text-embedding-3-small',
1802
+ * input: ['Hello world', 'Goodbye world']
1803
+ * });
1804
+ * response.data.forEach((item, i) => {
1805
+ * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
1806
+ * });
1807
+ *
1808
+ * // With custom dimensions (if supported by model)
1809
+ * const response = await client.ai.embeddings.create({
1810
+ * model: 'openai/text-embedding-3-small',
1811
+ * input: 'Hello world',
1812
+ * dimensions: 256
1813
+ * });
1814
+ *
1815
+ * // With base64 encoding format
1816
+ * const response = await client.ai.embeddings.create({
1817
+ * model: 'openai/text-embedding-3-small',
1818
+ * input: 'Hello world',
1819
+ * encoding_format: 'base64'
1820
+ * });
1821
+ * ```
1822
+ */
1823
+ async create(params) {
1824
+ const response = await this.http.post(
1825
+ "/api/ai/embeddings",
1826
+ params
1827
+ );
1828
+ return {
1829
+ object: response.object,
1830
+ data: response.data,
1831
+ model: response.metadata?.model,
1832
+ usage: response.metadata?.usage ? {
1833
+ prompt_tokens: response.metadata.usage.promptTokens || 0,
1834
+ total_tokens: response.metadata.usage.totalTokens || 0
1835
+ } : {
1836
+ prompt_tokens: 0,
1837
+ total_tokens: 0
1838
+ }
1839
+ };
1840
+ }
1841
+ };
1413
1842
  var Images = class {
1414
1843
  constructor(http) {
1415
1844
  this.http = http;
@@ -1467,30 +1896,73 @@ var Images = class {
1467
1896
  };
1468
1897
 
1469
1898
  // src/modules/functions.ts
1470
- var Functions = class {
1471
- constructor(http) {
1899
+ var Functions = class _Functions {
1900
+ constructor(http, functionsUrl) {
1472
1901
  this.http = http;
1902
+ this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
1903
+ }
1904
+ /**
1905
+ * Derive the subhosting URL from the base URL.
1906
+ * Base URL pattern: https://{appKey}.{region}.insforge.app
1907
+ * Functions URL: https://{appKey}.functions.insforge.app
1908
+ * Only applies to .insforge.app domains.
1909
+ */
1910
+ static deriveSubhostingUrl(baseUrl) {
1911
+ try {
1912
+ const { hostname } = new URL(baseUrl);
1913
+ if (!hostname.endsWith(".insforge.app")) return void 0;
1914
+ const appKey = hostname.split(".")[0];
1915
+ return `https://${appKey}.functions.insforge.app`;
1916
+ } catch {
1917
+ return void 0;
1918
+ }
1473
1919
  }
1474
1920
  /**
1475
1921
  * Invokes an Edge Function
1922
+ *
1923
+ * If functionsUrl is configured, tries direct subhosting first.
1924
+ * Falls back to proxy URL if subhosting returns 404.
1925
+ *
1476
1926
  * @param slug The function slug to invoke
1477
1927
  * @param options Request options
1478
1928
  */
1479
1929
  async invoke(slug, options = {}) {
1930
+ const { method = "POST", body, headers = {} } = options;
1931
+ if (this.functionsUrl) {
1932
+ try {
1933
+ const data = await this.http.request(method, `${this.functionsUrl}/${slug}`, {
1934
+ body,
1935
+ headers
1936
+ });
1937
+ return { data, error: null };
1938
+ } catch (error) {
1939
+ if (error instanceof Error && error.name === "AbortError") throw error;
1940
+ if (error instanceof InsForgeError && error.statusCode === 404) {
1941
+ } else {
1942
+ return {
1943
+ data: null,
1944
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1945
+ error instanceof Error ? error.message : "Function invocation failed",
1946
+ 500,
1947
+ "FUNCTION_ERROR"
1948
+ )
1949
+ };
1950
+ }
1951
+ }
1952
+ }
1480
1953
  try {
1481
- const { method = "POST", body, headers = {} } = options;
1482
1954
  const path = `/functions/${slug}`;
1483
- const data = await this.http.request(
1484
- method,
1485
- path,
1486
- { body, headers }
1487
- );
1955
+ const data = await this.http.request(method, path, { body, headers });
1488
1956
  return { data, error: null };
1489
1957
  } catch (error) {
1958
+ if (error instanceof Error && error.name === "AbortError") throw error;
1490
1959
  return {
1491
1960
  data: null,
1492
- error
1493
- // Pass through the full error object with all properties
1961
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1962
+ error instanceof Error ? error.message : "Function invocation failed",
1963
+ 500,
1964
+ "FUNCTION_ERROR"
1965
+ )
1494
1966
  };
1495
1967
  }
1496
1968
  }
@@ -1500,13 +1972,15 @@ var Functions = class {
1500
1972
  var import_socket = require("socket.io-client");
1501
1973
  var CONNECT_TIMEOUT = 1e4;
1502
1974
  var Realtime = class {
1503
- constructor(baseUrl, tokenManager) {
1975
+ constructor(baseUrl, tokenManager, anonKey) {
1504
1976
  this.socket = null;
1505
1977
  this.connectPromise = null;
1506
1978
  this.subscribedChannels = /* @__PURE__ */ new Set();
1507
1979
  this.eventListeners = /* @__PURE__ */ new Map();
1508
1980
  this.baseUrl = baseUrl;
1509
1981
  this.tokenManager = tokenManager;
1982
+ this.anonKey = anonKey;
1983
+ this.tokenManager.onTokenChange = () => this.onTokenChange();
1510
1984
  }
1511
1985
  notifyListeners(event, payload) {
1512
1986
  const listeners = this.eventListeners.get(event);
@@ -1531,8 +2005,7 @@ var Realtime = class {
1531
2005
  return this.connectPromise;
1532
2006
  }
1533
2007
  this.connectPromise = new Promise((resolve, reject) => {
1534
- const session = this.tokenManager.getSession();
1535
- const token = session?.accessToken;
2008
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
1536
2009
  this.socket = (0, import_socket.io)(this.baseUrl, {
1537
2010
  transports: ["websocket"],
1538
2011
  auth: token ? { token } : void 0
@@ -1598,6 +2071,21 @@ var Realtime = class {
1598
2071
  }
1599
2072
  this.subscribedChannels.clear();
1600
2073
  }
2074
+ /**
2075
+ * Handle token changes (e.g., after auth refresh)
2076
+ * Updates socket auth so reconnects use the new token
2077
+ * If connected, triggers reconnect to apply new token immediately
2078
+ */
2079
+ onTokenChange() {
2080
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2081
+ if (this.socket) {
2082
+ this.socket.auth = token ? { token } : {};
2083
+ }
2084
+ if (this.socket && (this.socket.connected || this.connectPromise)) {
2085
+ this.socket.disconnect();
2086
+ this.socket.connect();
2087
+ }
2088
+ }
1601
2089
  /**
1602
2090
  * Check if connected to the realtime server
1603
2091
  */
@@ -1746,8 +2234,15 @@ var Emails = class {
1746
2234
  );
1747
2235
  return { data, error: null };
1748
2236
  } catch (error) {
1749
- const normalizedError = error instanceof Error ? error : new Error(String(error));
1750
- return { data: null, error: normalizedError };
2237
+ if (error instanceof Error && error.name === "AbortError") throw error;
2238
+ return {
2239
+ data: null,
2240
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2241
+ error instanceof Error ? error.message : "Email send failed",
2242
+ 500,
2243
+ "EMAIL_ERROR"
2244
+ )
2245
+ };
1751
2246
  }
1752
2247
  }
1753
2248
  };
@@ -1755,31 +2250,30 @@ var Emails = class {
1755
2250
  // src/client.ts
1756
2251
  var InsForgeClient = class {
1757
2252
  constructor(config = {}) {
1758
- this.http = new HttpClient(config);
1759
- this.tokenManager = new TokenManager(config.storage);
2253
+ const logger = new Logger(config.debug);
2254
+ this.tokenManager = new TokenManager();
2255
+ this.http = new HttpClient(config, this.tokenManager, logger);
1760
2256
  if (config.edgeFunctionToken) {
1761
2257
  this.http.setAuthToken(config.edgeFunctionToken);
1762
- this.tokenManager.saveSession({
1763
- accessToken: config.edgeFunctionToken,
1764
- user: {}
1765
- // Will be populated by getCurrentUser()
1766
- });
1767
- }
1768
- const existingSession = this.tokenManager.getSession();
1769
- if (existingSession?.accessToken) {
1770
- this.http.setAuthToken(existingSession.accessToken);
2258
+ this.tokenManager.setAccessToken(config.edgeFunctionToken);
1771
2259
  }
1772
- this.auth = new Auth(this.http, this.tokenManager);
2260
+ this.auth = new Auth(this.http, this.tokenManager, {
2261
+ isServerMode: config.isServerMode ?? false
2262
+ });
1773
2263
  this.database = new Database(this.http, this.tokenManager);
1774
2264
  this.storage = new Storage(this.http);
1775
2265
  this.ai = new AI(this.http);
1776
- this.functions = new Functions(this.http);
1777
- this.realtime = new Realtime(this.http.baseUrl, this.tokenManager);
2266
+ this.functions = new Functions(this.http, config.functionsUrl);
2267
+ this.realtime = new Realtime(
2268
+ this.http.baseUrl,
2269
+ this.tokenManager,
2270
+ config.anonKey
2271
+ );
1778
2272
  this.emails = new Emails(this.http);
1779
2273
  }
1780
2274
  /**
1781
2275
  * Get the underlying HTTP client for custom requests
1782
- *
2276
+ *
1783
2277
  * @example
1784
2278
  * ```typescript
1785
2279
  * const httpClient = client.getHttpClient();
@@ -1814,6 +2308,7 @@ var index_default = InsForgeClient;
1814
2308
  HttpClient,
1815
2309
  InsForgeClient,
1816
2310
  InsForgeError,
2311
+ Logger,
1817
2312
  Realtime,
1818
2313
  Storage,
1819
2314
  StorageBucket,