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
@@ -17,127 +17,182 @@ var InsForgeError = class _InsForgeError extends Error {
17
17
  }
18
18
  };
19
19
 
20
- // src/lib/http-client.ts
21
- var HttpClient = class {
22
- constructor(config) {
23
- this.userToken = null;
24
- this.baseUrl = config.baseUrl || "http://localhost:7130";
25
- this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
26
- this.anonKey = config.anonKey;
27
- this.defaultHeaders = {
28
- ...config.headers
29
- };
30
- if (!this.fetch) {
31
- throw new Error(
32
- "Fetch is not available. Please provide a fetch implementation in the config."
33
- );
20
+ // src/lib/logger.ts
21
+ var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
22
+ var SENSITIVE_BODY_KEYS = [
23
+ "password",
24
+ "token",
25
+ "accesstoken",
26
+ "refreshtoken",
27
+ "authorization",
28
+ "secret",
29
+ "apikey",
30
+ "api_key",
31
+ "email",
32
+ "ssn",
33
+ "creditcard",
34
+ "credit_card"
35
+ ];
36
+ function redactHeaders(headers) {
37
+ const redacted = {};
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
40
+ redacted[key] = "***REDACTED***";
41
+ } else {
42
+ redacted[key] = value;
34
43
  }
35
44
  }
36
- buildUrl(path, params) {
37
- const url = new URL(path, this.baseUrl);
38
- if (params) {
39
- Object.entries(params).forEach(([key, value]) => {
40
- if (key === "select") {
41
- let normalizedValue = value.replace(/\s+/g, " ").trim();
42
- normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
43
- url.searchParams.append(key, normalizedValue);
44
- } else {
45
- url.searchParams.append(key, value);
46
- }
47
- });
45
+ return redacted;
46
+ }
47
+ function sanitizeBody(body) {
48
+ if (body === null || body === void 0) return body;
49
+ if (typeof body === "string") {
50
+ try {
51
+ const parsed = JSON.parse(body);
52
+ return sanitizeBody(parsed);
53
+ } catch {
54
+ return body;
48
55
  }
49
- return url.toString();
50
56
  }
51
- async request(method, path, options = {}) {
52
- const { params, headers = {}, body, ...fetchOptions } = options;
53
- const url = this.buildUrl(path, params);
54
- const requestHeaders = {
55
- ...this.defaultHeaders
56
- };
57
- const authToken = this.userToken || this.anonKey;
58
- if (authToken) {
59
- requestHeaders["Authorization"] = `Bearer ${authToken}`;
60
- }
61
- let processedBody;
62
- if (body !== void 0) {
63
- if (typeof FormData !== "undefined" && body instanceof FormData) {
64
- processedBody = body;
57
+ if (Array.isArray(body)) return body.map(sanitizeBody);
58
+ if (typeof body === "object") {
59
+ const sanitized = {};
60
+ for (const [key, value] of Object.entries(body)) {
61
+ if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
62
+ sanitized[key] = "***REDACTED***";
65
63
  } else {
66
- if (method !== "GET") {
67
- requestHeaders["Content-Type"] = "application/json;charset=UTF-8";
68
- }
69
- processedBody = JSON.stringify(body);
64
+ sanitized[key] = sanitizeBody(value);
70
65
  }
71
66
  }
72
- Object.assign(requestHeaders, headers);
73
- const response = await this.fetch(url, {
74
- method,
75
- headers: requestHeaders,
76
- body: processedBody,
77
- ...fetchOptions
78
- });
79
- if (response.status === 204) {
80
- return void 0;
81
- }
82
- let data;
83
- const contentType = response.headers.get("content-type");
84
- if (contentType?.includes("json")) {
85
- data = await response.json();
86
- } else {
87
- data = await response.text();
88
- }
89
- if (!response.ok) {
90
- if (data && typeof data === "object" && "error" in data) {
91
- if (!data.statusCode && !data.status) {
92
- data.statusCode = response.status;
93
- }
94
- const error = InsForgeError.fromApiError(data);
95
- Object.keys(data).forEach((key) => {
96
- if (key !== "error" && key !== "message" && key !== "statusCode") {
97
- error[key] = data[key];
98
- }
99
- });
100
- throw error;
101
- }
102
- throw new InsForgeError(
103
- `Request failed: ${response.statusText}`,
104
- response.status,
105
- "REQUEST_FAILED"
106
- );
67
+ return sanitized;
68
+ }
69
+ return body;
70
+ }
71
+ function formatBody(body) {
72
+ if (body === void 0 || body === null) return "";
73
+ if (typeof body === "string") {
74
+ try {
75
+ return JSON.stringify(JSON.parse(body), null, 2);
76
+ } catch {
77
+ return body;
107
78
  }
108
- return data;
109
79
  }
110
- get(path, options) {
111
- return this.request("GET", path, options);
80
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
81
+ return "[FormData]";
112
82
  }
113
- post(path, body, options) {
114
- return this.request("POST", path, { ...options, body });
83
+ try {
84
+ return JSON.stringify(body, null, 2);
85
+ } catch {
86
+ return "[Unserializable body]";
115
87
  }
116
- put(path, body, options) {
117
- return this.request("PUT", path, { ...options, body });
88
+ }
89
+ var Logger = class {
90
+ /**
91
+ * Creates a new Logger instance.
92
+ * @param debug - Set to true to enable console logging, or pass a custom log function
93
+ */
94
+ constructor(debug) {
95
+ if (typeof debug === "function") {
96
+ this.enabled = true;
97
+ this.customLog = debug;
98
+ } else {
99
+ this.enabled = !!debug;
100
+ this.customLog = null;
101
+ }
118
102
  }
119
- patch(path, body, options) {
120
- return this.request("PATCH", path, { ...options, body });
103
+ /**
104
+ * Logs a debug message at the info level.
105
+ * @param message - The message to log
106
+ * @param args - Additional arguments to pass to the log function
107
+ */
108
+ log(message, ...args) {
109
+ if (!this.enabled) return;
110
+ const formatted = `[InsForge Debug] ${message}`;
111
+ if (this.customLog) {
112
+ this.customLog(formatted, ...args);
113
+ } else {
114
+ console.log(formatted, ...args);
115
+ }
121
116
  }
122
- delete(path, options) {
123
- return this.request("DELETE", path, options);
117
+ /**
118
+ * Logs a debug message at the warning level.
119
+ * @param message - The message to log
120
+ * @param args - Additional arguments to pass to the log function
121
+ */
122
+ warn(message, ...args) {
123
+ if (!this.enabled) return;
124
+ const formatted = `[InsForge Debug] ${message}`;
125
+ if (this.customLog) {
126
+ this.customLog(formatted, ...args);
127
+ } else {
128
+ console.warn(formatted, ...args);
129
+ }
124
130
  }
125
- setAuthToken(token) {
126
- this.userToken = token;
131
+ /**
132
+ * Logs a debug message at the error level.
133
+ * @param message - The message to log
134
+ * @param args - Additional arguments to pass to the log function
135
+ */
136
+ error(message, ...args) {
137
+ if (!this.enabled) return;
138
+ const formatted = `[InsForge Debug] ${message}`;
139
+ if (this.customLog) {
140
+ this.customLog(formatted, ...args);
141
+ } else {
142
+ console.error(formatted, ...args);
143
+ }
127
144
  }
128
- getHeaders() {
129
- const headers = { ...this.defaultHeaders };
130
- const authToken = this.userToken || this.anonKey;
131
- if (authToken) {
132
- headers["Authorization"] = `Bearer ${authToken}`;
145
+ /**
146
+ * Logs an outgoing HTTP request with method, URL, headers, and body.
147
+ * Sensitive headers and body fields are automatically redacted.
148
+ * @param method - HTTP method (GET, POST, etc.)
149
+ * @param url - The full request URL
150
+ * @param headers - Request headers (sensitive values will be redacted)
151
+ * @param body - Request body (sensitive fields will be masked)
152
+ */
153
+ logRequest(method, url, headers, body) {
154
+ if (!this.enabled) return;
155
+ const parts = [
156
+ `\u2192 ${method} ${url}`
157
+ ];
158
+ if (headers && Object.keys(headers).length > 0) {
159
+ parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
160
+ }
161
+ const formattedBody = formatBody(sanitizeBody(body));
162
+ if (formattedBody) {
163
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
164
+ parts.push(` Body: ${truncated}`);
165
+ }
166
+ this.log(parts.join("\n"));
167
+ }
168
+ /**
169
+ * Logs an incoming HTTP response with method, URL, status, duration, and body.
170
+ * Error responses (4xx/5xx) are logged at the error level.
171
+ * @param method - HTTP method (GET, POST, etc.)
172
+ * @param url - The full request URL
173
+ * @param status - HTTP response status code
174
+ * @param durationMs - Request duration in milliseconds
175
+ * @param body - Response body (sensitive fields will be masked, large bodies truncated)
176
+ */
177
+ logResponse(method, url, status, durationMs, body) {
178
+ if (!this.enabled) return;
179
+ const parts = [
180
+ `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
181
+ ];
182
+ const formattedBody = formatBody(sanitizeBody(body));
183
+ if (formattedBody) {
184
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
185
+ parts.push(` Body: ${truncated}`);
186
+ }
187
+ if (status >= 400) {
188
+ this.error(parts.join("\n"));
189
+ } else {
190
+ this.log(parts.join("\n"));
133
191
  }
134
- return headers;
135
192
  }
136
193
  };
137
194
 
138
195
  // src/lib/token-manager.ts
139
- var TOKEN_KEY = "insforge-auth-token";
140
- var USER_KEY = "insforge-auth-user";
141
196
  var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
142
197
  function getCsrfToken() {
143
198
  if (typeof document === "undefined") return null;
@@ -157,84 +212,28 @@ function clearCsrfToken() {
157
212
  document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
158
213
  }
159
214
  var TokenManager = class {
160
- constructor(storage) {
215
+ constructor() {
161
216
  // In-memory storage
162
217
  this.accessToken = null;
163
218
  this.user = null;
164
- // Mode: 'memory' (new backend) or 'storage' (legacy backend, default)
165
- this._mode = "storage";
166
- if (storage) {
167
- this.storage = storage;
168
- } else if (typeof window !== "undefined" && window.localStorage) {
169
- this.storage = window.localStorage;
170
- } else {
171
- const store = /* @__PURE__ */ new Map();
172
- this.storage = {
173
- getItem: (key) => store.get(key) || null,
174
- setItem: (key, value) => {
175
- store.set(key, value);
176
- },
177
- removeItem: (key) => {
178
- store.delete(key);
179
- }
180
- };
181
- }
182
- }
183
- /**
184
- * Get current mode
185
- */
186
- get mode() {
187
- return this._mode;
188
- }
189
- /**
190
- * Set mode to memory (new backend with cookies + memory)
191
- */
192
- setMemoryMode() {
193
- if (this._mode === "storage") {
194
- this.storage.removeItem(TOKEN_KEY);
195
- this.storage.removeItem(USER_KEY);
196
- }
197
- this._mode = "memory";
198
- }
199
- /**
200
- * Set mode to storage (legacy backend with localStorage)
201
- * Also loads existing session from localStorage
202
- */
203
- setStorageMode() {
204
- this._mode = "storage";
205
- this.loadFromStorage();
219
+ // Callback for token changes (used by realtime to reconnect with new token)
220
+ this.onTokenChange = null;
206
221
  }
207
222
  /**
208
- * Load session from localStorage
209
- */
210
- loadFromStorage() {
211
- const token = this.storage.getItem(TOKEN_KEY);
212
- const userStr = this.storage.getItem(USER_KEY);
213
- if (token && userStr) {
214
- try {
215
- this.accessToken = token;
216
- this.user = JSON.parse(userStr);
217
- } catch {
218
- this.clearSession();
219
- }
220
- }
221
- }
222
- /**
223
- * Save session (memory always, localStorage only in storage mode)
223
+ * Save session in memory
224
224
  */
225
225
  saveSession(session) {
226
+ const tokenChanged = session.accessToken !== this.accessToken;
226
227
  this.accessToken = session.accessToken;
227
228
  this.user = session.user;
228
- if (this._mode === "storage") {
229
- this.storage.setItem(TOKEN_KEY, session.accessToken);
230
- this.storage.setItem(USER_KEY, JSON.stringify(session.user));
229
+ if (tokenChanged && this.onTokenChange) {
230
+ this.onTokenChange();
231
231
  }
232
232
  }
233
233
  /**
234
234
  * Get current session
235
235
  */
236
236
  getSession() {
237
- this.loadFromStorage();
238
237
  if (!this.accessToken || !this.user) return null;
239
238
  return {
240
239
  accessToken: this.accessToken,
@@ -245,16 +244,16 @@ var TokenManager = class {
245
244
  * Get access token
246
245
  */
247
246
  getAccessToken() {
248
- this.loadFromStorage();
249
247
  return this.accessToken;
250
248
  }
251
249
  /**
252
250
  * Set access token
253
251
  */
254
252
  setAccessToken(token) {
253
+ const tokenChanged = token !== this.accessToken;
255
254
  this.accessToken = token;
256
- if (this._mode === "storage") {
257
- this.storage.setItem(TOKEN_KEY, token);
255
+ if (tokenChanged && this.onTokenChange) {
256
+ this.onTokenChange();
258
257
  }
259
258
  }
260
259
  /**
@@ -268,479 +267,599 @@ var TokenManager = class {
268
267
  */
269
268
  setUser(user) {
270
269
  this.user = user;
271
- if (this._mode === "storage") {
272
- this.storage.setItem(USER_KEY, JSON.stringify(user));
273
- }
274
270
  }
275
271
  /**
276
- * Clear session (both memory and localStorage)
272
+ * Clear in-memory session
277
273
  */
278
274
  clearSession() {
275
+ const hadToken = this.accessToken !== null;
279
276
  this.accessToken = null;
280
277
  this.user = null;
281
- this.storage.removeItem(TOKEN_KEY);
282
- this.storage.removeItem(USER_KEY);
283
- }
284
- /**
285
- * Check if there's a session in localStorage (for legacy detection)
286
- */
287
- hasStoredSession() {
288
- const token = this.storage.getItem(TOKEN_KEY);
289
- return !!token;
278
+ if (hadToken && this.onTokenChange) {
279
+ this.onTokenChange();
280
+ }
290
281
  }
291
282
  };
292
283
 
293
- // src/modules/auth.ts
294
- function isHostedAuthEnvironment() {
295
- if (typeof window === "undefined") {
296
- return false;
297
- }
298
- const { hostname, port, protocol } = window.location;
299
- if (hostname === "localhost" && port === "7130") {
300
- return true;
301
- }
302
- if (protocol === "https:" && hostname.endsWith(".insforge.app")) {
303
- return true;
304
- }
305
- return false;
306
- }
307
- var Auth = class {
308
- constructor(http, tokenManager) {
309
- this.http = http;
310
- this.tokenManager = tokenManager;
311
- this.detectAuthCallback();
312
- }
284
+ // src/lib/http-client.ts
285
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
286
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
287
+ var HttpClient = class {
313
288
  /**
314
- * Automatically detect and handle OAuth callback parameters in the URL
315
- * This runs after initialization to seamlessly complete the OAuth flow
316
- * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544)
289
+ * Creates a new HttpClient instance.
290
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
291
+ * @param tokenManager - Token manager for session persistence.
292
+ * @param logger - Optional logger instance for request/response debugging.
317
293
  */
318
- detectAuthCallback() {
319
- if (typeof window === "undefined") return;
320
- try {
321
- const params = new URLSearchParams(window.location.search);
322
- const accessToken = params.get("access_token");
323
- const userId = params.get("user_id");
324
- const email = params.get("email");
325
- const name = params.get("name");
326
- const csrfToken = params.get("csrf_token");
327
- if (accessToken && userId && email) {
328
- if (csrfToken) {
329
- this.tokenManager.setMemoryMode();
330
- setCsrfToken(csrfToken);
331
- }
332
- const session = {
333
- accessToken,
334
- user: {
335
- id: userId,
336
- email,
337
- profile: { name: name || "" },
338
- metadata: null,
339
- // These fields are not provided by backend OAuth callback
340
- // They'll be populated when calling getCurrentUser()
341
- emailVerified: false,
342
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
343
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
344
- }
345
- };
346
- this.tokenManager.saveSession(session);
347
- this.http.setAuthToken(accessToken);
348
- const url = new URL(window.location.href);
349
- url.searchParams.delete("access_token");
350
- url.searchParams.delete("user_id");
351
- url.searchParams.delete("email");
352
- url.searchParams.delete("name");
353
- url.searchParams.delete("csrf_token");
354
- if (params.has("error")) {
355
- url.searchParams.delete("error");
356
- }
357
- window.history.replaceState({}, document.title, url.toString());
358
- }
359
- } catch (error) {
360
- console.debug("OAuth callback detection skipped:", error);
294
+ constructor(config, tokenManager, logger) {
295
+ this.userToken = null;
296
+ this.autoRefreshToken = true;
297
+ this.isRefreshing = false;
298
+ this.refreshPromise = null;
299
+ this.refreshToken = null;
300
+ this.baseUrl = config.baseUrl || "http://localhost:7130";
301
+ this.autoRefreshToken = config.autoRefreshToken ?? true;
302
+ this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
303
+ this.anonKey = config.anonKey;
304
+ this.defaultHeaders = {
305
+ ...config.headers
306
+ };
307
+ this.tokenManager = tokenManager ?? new TokenManager();
308
+ this.logger = logger || new Logger(false);
309
+ this.timeout = config.timeout ?? 3e4;
310
+ this.retryCount = config.retryCount ?? 3;
311
+ this.retryDelay = config.retryDelay ?? 500;
312
+ if (!this.fetch) {
313
+ throw new Error(
314
+ "Fetch is not available. Please provide a fetch implementation in the config."
315
+ );
361
316
  }
362
317
  }
363
318
  /**
364
- * Sign up a new user
319
+ * Builds a full URL from a path and optional query parameters.
320
+ * Normalizes PostgREST select parameters for proper syntax.
365
321
  */
366
- async signUp(request) {
367
- try {
368
- const response = await this.http.post("/api/auth/users", request);
369
- if (response.accessToken && response.user && !isHostedAuthEnvironment()) {
370
- const session = {
371
- accessToken: response.accessToken,
372
- user: response.user
373
- };
374
- this.tokenManager.saveSession(session);
375
- this.http.setAuthToken(response.accessToken);
376
- if (response.csrfToken) {
377
- setCsrfToken(response.csrfToken);
322
+ buildUrl(path, params) {
323
+ const url = new URL(path, this.baseUrl);
324
+ if (params) {
325
+ Object.entries(params).forEach(([key, value]) => {
326
+ if (key === "select") {
327
+ let normalizedValue = value.replace(/\s+/g, " ").trim();
328
+ normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
329
+ url.searchParams.append(key, normalizedValue);
330
+ } else {
331
+ url.searchParams.append(key, value);
378
332
  }
379
- }
380
- return {
381
- data: response,
382
- error: null
383
- };
384
- } catch (error) {
385
- if (error instanceof InsForgeError) {
386
- return { data: null, error };
387
- }
388
- return {
389
- data: null,
390
- error: new InsForgeError(
391
- error instanceof Error ? error.message : "An unexpected error occurred during sign up",
392
- 500,
393
- "UNEXPECTED_ERROR"
394
- )
395
- };
333
+ });
396
334
  }
335
+ return url.toString();
336
+ }
337
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
338
+ isRetryableStatus(status) {
339
+ return RETRYABLE_STATUS_CODES.has(status);
397
340
  }
398
341
  /**
399
- * Sign in with email and password
342
+ * Computes the delay before the next retry using exponential backoff with jitter.
343
+ * @param attempt - The current retry attempt number (1-based).
344
+ * @returns Delay in milliseconds.
400
345
  */
401
- async signInWithPassword(request) {
402
- try {
403
- const response = await this.http.post("/api/auth/sessions", request);
404
- if (!isHostedAuthEnvironment()) {
405
- const session = {
406
- accessToken: response.accessToken,
407
- user: response.user
408
- };
409
- this.tokenManager.saveSession(session);
410
- this.http.setAuthToken(response.accessToken);
411
- if (response.csrfToken) {
412
- setCsrfToken(response.csrfToken);
413
- }
414
- }
415
- return {
416
- data: response,
417
- error: null
418
- };
419
- } catch (error) {
420
- if (error instanceof InsForgeError) {
421
- return { data: null, error };
422
- }
423
- return {
424
- data: null,
425
- error: new InsForgeError(
426
- "An unexpected error occurred during sign in",
427
- 500,
428
- "UNEXPECTED_ERROR"
429
- )
430
- };
431
- }
346
+ computeRetryDelay(attempt) {
347
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
348
+ const jitter = base * (0.85 + Math.random() * 0.3);
349
+ return Math.round(jitter);
432
350
  }
433
351
  /**
434
- * Sign in with OAuth provider
352
+ * Performs an HTTP request with automatic retry and timeout handling.
353
+ * Retries on network errors and 5xx server errors with exponential backoff.
354
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
355
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
356
+ * @param path - API path relative to the base URL.
357
+ * @param options - Optional request configuration including headers, body, and query params.
358
+ * @returns Parsed response data.
359
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
435
360
  */
436
- async signInWithOAuth(options) {
437
- try {
438
- const { provider, redirectTo, skipBrowserRedirect } = options;
439
- const params = redirectTo ? { redirect_uri: redirectTo } : void 0;
440
- const endpoint = `/api/auth/oauth/${provider}`;
441
- const response = await this.http.get(endpoint, { params });
442
- if (typeof window !== "undefined" && !skipBrowserRedirect) {
443
- window.location.href = response.authUrl;
444
- return { data: {}, error: null };
445
- }
446
- return {
447
- data: {
448
- url: response.authUrl,
449
- provider
450
- },
451
- error: null
452
- };
453
- } catch (error) {
454
- if (error instanceof InsForgeError) {
455
- return { data: {}, error };
361
+ async handleRequest(method, path, options = {}) {
362
+ const {
363
+ params,
364
+ headers = {},
365
+ body,
366
+ signal: callerSignal,
367
+ ...fetchOptions
368
+ } = options;
369
+ const url = this.buildUrl(path, params);
370
+ const startTime = Date.now();
371
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
372
+ const maxAttempts = canRetry ? this.retryCount : 0;
373
+ const requestHeaders = {
374
+ ...this.defaultHeaders
375
+ };
376
+ const authToken = this.userToken || this.anonKey;
377
+ if (authToken) {
378
+ requestHeaders["Authorization"] = `Bearer ${authToken}`;
379
+ }
380
+ let processedBody;
381
+ if (body !== void 0) {
382
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
383
+ processedBody = body;
384
+ } else {
385
+ if (method !== "GET") {
386
+ requestHeaders["Content-Type"] = "application/json;charset=UTF-8";
387
+ }
388
+ processedBody = JSON.stringify(body);
456
389
  }
457
- return {
458
- data: {},
459
- error: new InsForgeError(
460
- "An unexpected error occurred during OAuth initialization",
461
- 500,
462
- "UNEXPECTED_ERROR"
463
- )
464
- };
465
390
  }
466
- }
467
- /**
468
- * Sign out the current user
469
- */
470
- async signOut() {
471
- try {
391
+ if (headers instanceof Headers) {
392
+ headers.forEach((value, key) => {
393
+ requestHeaders[key] = value;
394
+ });
395
+ } else if (Array.isArray(headers)) {
396
+ headers.forEach(([key, value]) => {
397
+ requestHeaders[key] = value;
398
+ });
399
+ } else {
400
+ Object.assign(requestHeaders, headers);
401
+ }
402
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
403
+ let lastError;
404
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
405
+ if (attempt > 0) {
406
+ const delay = this.computeRetryDelay(attempt);
407
+ this.logger.warn(
408
+ `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
409
+ );
410
+ if (callerSignal?.aborted) throw callerSignal.reason;
411
+ await new Promise((resolve, reject) => {
412
+ const onAbort = () => {
413
+ clearTimeout(timer2);
414
+ reject(callerSignal.reason);
415
+ };
416
+ const timer2 = setTimeout(() => {
417
+ if (callerSignal)
418
+ callerSignal.removeEventListener("abort", onAbort);
419
+ resolve();
420
+ }, delay);
421
+ if (callerSignal) {
422
+ callerSignal.addEventListener("abort", onAbort, { once: true });
423
+ }
424
+ });
425
+ }
426
+ let controller;
427
+ let timer;
428
+ if (this.timeout > 0 || callerSignal) {
429
+ controller = new AbortController();
430
+ if (this.timeout > 0) {
431
+ timer = setTimeout(() => controller.abort(), this.timeout);
432
+ }
433
+ if (callerSignal) {
434
+ if (callerSignal.aborted) {
435
+ controller.abort(callerSignal.reason);
436
+ } else {
437
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
438
+ callerSignal.addEventListener("abort", onCallerAbort, {
439
+ once: true
440
+ });
441
+ controller.signal.addEventListener(
442
+ "abort",
443
+ () => {
444
+ callerSignal.removeEventListener("abort", onCallerAbort);
445
+ },
446
+ { once: true }
447
+ );
448
+ }
449
+ }
450
+ }
472
451
  try {
473
- await this.http.post("/api/auth/logout", void 0, { credentials: "include" });
474
- } catch {
452
+ const response = await this.fetch(url, {
453
+ method,
454
+ headers: requestHeaders,
455
+ body: processedBody,
456
+ ...fetchOptions,
457
+ ...controller ? { signal: controller.signal } : {}
458
+ });
459
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
460
+ if (timer !== void 0) clearTimeout(timer);
461
+ await response.body?.cancel();
462
+ lastError = new InsForgeError(
463
+ `Server error: ${response.status} ${response.statusText}`,
464
+ response.status,
465
+ "SERVER_ERROR"
466
+ );
467
+ continue;
468
+ }
469
+ if (response.status === 204) {
470
+ if (timer !== void 0) clearTimeout(timer);
471
+ return void 0;
472
+ }
473
+ let data;
474
+ const contentType = response.headers.get("content-type");
475
+ try {
476
+ if (contentType?.includes("json")) {
477
+ data = await response.json();
478
+ } else {
479
+ data = await response.text();
480
+ }
481
+ } catch (parseErr) {
482
+ if (timer !== void 0) clearTimeout(timer);
483
+ throw new InsForgeError(
484
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
485
+ response.status,
486
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
487
+ );
488
+ }
489
+ if (timer !== void 0) clearTimeout(timer);
490
+ if (!response.ok) {
491
+ this.logger.logResponse(
492
+ method,
493
+ url,
494
+ response.status,
495
+ Date.now() - startTime,
496
+ data
497
+ );
498
+ if (data && typeof data === "object" && "error" in data) {
499
+ if (!data.statusCode && !data.status) {
500
+ data.statusCode = response.status;
501
+ }
502
+ const error = InsForgeError.fromApiError(data);
503
+ Object.keys(data).forEach((key) => {
504
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
505
+ error[key] = data[key];
506
+ }
507
+ });
508
+ throw error;
509
+ }
510
+ throw new InsForgeError(
511
+ `Request failed: ${response.statusText}`,
512
+ response.status,
513
+ "REQUEST_FAILED"
514
+ );
515
+ }
516
+ this.logger.logResponse(
517
+ method,
518
+ url,
519
+ response.status,
520
+ Date.now() - startTime,
521
+ data
522
+ );
523
+ return data;
524
+ } catch (err) {
525
+ if (timer !== void 0) clearTimeout(timer);
526
+ if (err?.name === "AbortError") {
527
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
528
+ throw new InsForgeError(
529
+ `Request timed out after ${this.timeout}ms`,
530
+ 408,
531
+ "REQUEST_TIMEOUT"
532
+ );
533
+ }
534
+ throw err;
535
+ }
536
+ if (err instanceof InsForgeError) {
537
+ throw err;
538
+ }
539
+ if (attempt < maxAttempts) {
540
+ lastError = err;
541
+ continue;
542
+ }
543
+ throw new InsForgeError(
544
+ `Network request failed: ${err?.message || "Unknown error"}`,
545
+ 0,
546
+ "NETWORK_ERROR"
547
+ );
475
548
  }
476
- this.tokenManager.clearSession();
477
- this.http.setAuthToken(null);
478
- clearCsrfToken();
479
- return { error: null };
480
- } catch (error) {
481
- return {
482
- error: new InsForgeError(
483
- "Failed to sign out",
484
- 500,
485
- "SIGNOUT_ERROR"
486
- )
487
- };
488
549
  }
550
+ throw lastError || new InsForgeError(
551
+ "Request failed after all retry attempts",
552
+ 0,
553
+ "NETWORK_ERROR"
554
+ );
489
555
  }
490
- /**
491
- * Get all public authentication configuration (OAuth + Email)
492
- * Returns both OAuth providers and email authentication settings in one request
493
- * This is a public endpoint that doesn't require authentication
494
- *
495
- * @returns Complete public authentication configuration including OAuth providers and email auth settings
496
- *
497
- * @example
498
- * ```ts
499
- * const { data, error } = await insforge.auth.getPublicAuthConfig();
500
- * if (data) {
501
- * console.log(`OAuth providers: ${data.oauth.data.length}`);
502
- * console.log(`Password min length: ${data.email.passwordMinLength}`);
503
- * }
504
- * ```
505
- */
506
- async getPublicAuthConfig() {
556
+ async request(method, path, options = {}) {
507
557
  try {
508
- const response = await this.http.get("/api/auth/public-config");
509
- return {
510
- data: response,
511
- error: null
512
- };
558
+ return await this.handleRequest(method, path, { ...options });
513
559
  } catch (error) {
514
- if (error instanceof InsForgeError) {
515
- return { data: null, error };
560
+ if (error instanceof InsForgeError && error.statusCode === 401 && error.error === "INVALID_TOKEN" && this.autoRefreshToken) {
561
+ try {
562
+ const newTokenData = await this.handleTokenRefresh();
563
+ this.setAuthToken(newTokenData.accessToken);
564
+ this.tokenManager.saveSession(newTokenData);
565
+ if (newTokenData.csrfToken) {
566
+ setCsrfToken(newTokenData.csrfToken);
567
+ }
568
+ if (newTokenData.refreshToken) {
569
+ this.setRefreshToken(newTokenData.refreshToken);
570
+ }
571
+ return await this.handleRequest(method, path, { ...options });
572
+ } catch (error2) {
573
+ this.tokenManager.clearSession();
574
+ this.userToken = null;
575
+ this.refreshToken = null;
576
+ clearCsrfToken();
577
+ throw error2;
578
+ }
516
579
  }
517
- return {
518
- data: null,
519
- error: new InsForgeError(
520
- "An unexpected error occurred while fetching public authentication configuration",
521
- 500,
522
- "UNEXPECTED_ERROR"
523
- )
524
- };
580
+ throw error;
525
581
  }
526
582
  }
527
- /**
528
- * Get the current user with full profile information
529
- * Returns both auth info (id, email, role) and profile data (dynamic fields from users table)
530
- */
531
- async getCurrentUser() {
532
- try {
533
- const user = this.tokenManager.getUser();
534
- if (user) {
535
- return { data: { user }, error: null };
536
- }
537
- const accessToken = this.tokenManager.getAccessToken();
538
- if (!accessToken) {
539
- return { data: null, error: null };
540
- }
541
- this.http.setAuthToken(accessToken);
542
- const authResponse = await this.http.get("/api/auth/sessions/current");
543
- return {
544
- data: {
545
- user: authResponse.user
546
- },
547
- error: null
548
- };
549
- } catch (error) {
550
- if (error instanceof InsForgeError && error.statusCode === 401) {
551
- await this.signOut();
552
- return { data: null, error: null };
553
- }
554
- if (error instanceof InsForgeError) {
555
- return { data: null, error };
556
- }
557
- return {
558
- data: null,
559
- error: new InsForgeError(
560
- "An unexpected error occurred while fetching user",
561
- 500,
562
- "UNEXPECTED_ERROR"
563
- )
564
- };
583
+ /** Performs a GET request. */
584
+ get(path, options) {
585
+ return this.request("GET", path, options);
586
+ }
587
+ /** Performs a POST request with an optional JSON body. */
588
+ post(path, body, options) {
589
+ return this.request("POST", path, { ...options, body });
590
+ }
591
+ /** Performs a PUT request with an optional JSON body. */
592
+ put(path, body, options) {
593
+ return this.request("PUT", path, { ...options, body });
594
+ }
595
+ /** Performs a PATCH request with an optional JSON body. */
596
+ patch(path, body, options) {
597
+ return this.request("PATCH", path, { ...options, body });
598
+ }
599
+ /** Performs a DELETE request. */
600
+ delete(path, options) {
601
+ return this.request("DELETE", path, options);
602
+ }
603
+ /** Sets or clears the user authentication token for subsequent requests. */
604
+ setAuthToken(token) {
605
+ this.userToken = token;
606
+ }
607
+ setRefreshToken(token) {
608
+ this.refreshToken = token;
609
+ }
610
+ /** Returns the current default headers including the authorization header if set. */
611
+ getHeaders() {
612
+ const headers = { ...this.defaultHeaders };
613
+ const authToken = this.userToken || this.anonKey;
614
+ if (authToken) {
615
+ headers["Authorization"] = `Bearer ${authToken}`;
616
+ }
617
+ return headers;
618
+ }
619
+ async handleTokenRefresh() {
620
+ if (this.isRefreshing) {
621
+ return this.refreshPromise;
565
622
  }
623
+ this.isRefreshing = true;
624
+ this.refreshPromise = (async () => {
625
+ try {
626
+ const csrfToken = getCsrfToken();
627
+ const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
628
+ const response = await this.handleRequest(
629
+ "POST",
630
+ "/api/auth/sessions/current",
631
+ {
632
+ body,
633
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
634
+ credentials: "include"
635
+ }
636
+ );
637
+ return response;
638
+ } finally {
639
+ this.isRefreshing = false;
640
+ this.refreshPromise = null;
641
+ }
642
+ })();
643
+ return this.refreshPromise;
644
+ }
645
+ };
646
+
647
+ // src/modules/auth/helpers.ts
648
+ var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
649
+ function base64UrlEncode(buffer) {
650
+ const base64 = btoa(String.fromCharCode(...buffer));
651
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
652
+ }
653
+ function generateCodeVerifier() {
654
+ const array = new Uint8Array(32);
655
+ crypto.getRandomValues(array);
656
+ return base64UrlEncode(array);
657
+ }
658
+ async function generateCodeChallenge(verifier) {
659
+ const encoder = new TextEncoder();
660
+ const data = encoder.encode(verifier);
661
+ const hash = await crypto.subtle.digest("SHA-256", data);
662
+ return base64UrlEncode(new Uint8Array(hash));
663
+ }
664
+ function storePkceVerifier(verifier) {
665
+ if (typeof sessionStorage !== "undefined") {
666
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
667
+ }
668
+ }
669
+ function retrievePkceVerifier() {
670
+ if (typeof sessionStorage === "undefined") {
671
+ return null;
672
+ }
673
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
674
+ if (verifier) {
675
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
676
+ }
677
+ return verifier;
678
+ }
679
+ function wrapError(error, fallbackMessage) {
680
+ if (error instanceof InsForgeError) {
681
+ return { data: null, error };
682
+ }
683
+ return {
684
+ data: null,
685
+ error: new InsForgeError(
686
+ error instanceof Error ? error.message : fallbackMessage,
687
+ 500,
688
+ "UNEXPECTED_ERROR"
689
+ )
690
+ };
691
+ }
692
+ function cleanUrlParams(...params) {
693
+ if (typeof window === "undefined") {
694
+ return;
695
+ }
696
+ const url = new URL(window.location.href);
697
+ params.forEach((p) => url.searchParams.delete(p));
698
+ window.history.replaceState({}, document.title, url.toString());
699
+ }
700
+
701
+ // src/modules/auth/auth.ts
702
+ import { oAuthProvidersSchema } from "@insforge/shared-schemas";
703
+ var Auth = class {
704
+ constructor(http, tokenManager, options = {}) {
705
+ this.http = http;
706
+ this.tokenManager = tokenManager;
707
+ this.options = options;
708
+ this.authCallbackHandled = this.detectAuthCallback();
709
+ }
710
+ isServerMode() {
711
+ return !!this.options.isServerMode;
566
712
  }
567
713
  /**
568
- * Get any user's profile by ID
569
- * Returns profile information from the users table
714
+ * Save session from API response
715
+ * Handles token storage, CSRF token, and HTTP auth header
570
716
  */
571
- async getProfile(userId) {
572
- try {
573
- const response = await this.http.get(`/api/auth/profiles/${userId}`);
574
- return {
575
- data: response,
576
- error: null
577
- };
578
- } catch (error) {
579
- if (error instanceof InsForgeError) {
580
- return { data: null, error };
581
- }
582
- return {
583
- data: null,
584
- error: new InsForgeError(
585
- "An unexpected error occurred while fetching user profile",
586
- 500,
587
- "UNEXPECTED_ERROR"
588
- )
589
- };
717
+ saveSessionFromResponse(response) {
718
+ if (!response.accessToken || !response.user) {
719
+ return false;
720
+ }
721
+ const session = {
722
+ accessToken: response.accessToken,
723
+ user: response.user
724
+ };
725
+ if (!this.isServerMode() && response.csrfToken) {
726
+ setCsrfToken(response.csrfToken);
727
+ }
728
+ if (!this.isServerMode()) {
729
+ this.tokenManager.saveSession(session);
590
730
  }
731
+ this.http.setAuthToken(response.accessToken);
732
+ this.http.setRefreshToken(response.refreshToken ?? null);
733
+ return true;
591
734
  }
735
+ // ============================================================================
736
+ // OAuth Callback Detection (runs on initialization)
737
+ // ============================================================================
592
738
  /**
593
- * Get the current session (only session data, no API call)
594
- * Returns the stored JWT token and basic user info from local storage
739
+ * Detect and handle OAuth callback parameters in URL
740
+ * Supports PKCE flow (insforge_code)
595
741
  */
596
- async getCurrentSession() {
742
+ async detectAuthCallback() {
743
+ if (this.isServerMode() || typeof window === "undefined") return;
597
744
  try {
598
- const session = this.tokenManager.getSession();
599
- if (session) {
600
- this.http.setAuthToken(session.accessToken);
601
- return { data: { session }, error: null };
745
+ const params = new URLSearchParams(window.location.search);
746
+ const error = params.get("error");
747
+ if (error) {
748
+ cleanUrlParams("error");
749
+ console.debug("OAuth callback error:", error);
750
+ return;
602
751
  }
603
- if (typeof window !== "undefined") {
604
- try {
605
- const csrfToken = getCsrfToken();
606
- const response = await this.http.post(
607
- "/api/auth/refresh",
608
- void 0,
609
- {
610
- headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
611
- credentials: "include"
612
- }
613
- );
614
- if (response.accessToken) {
615
- this.tokenManager.setMemoryMode();
616
- this.tokenManager.setAccessToken(response.accessToken);
617
- this.http.setAuthToken(response.accessToken);
618
- if (response.user) {
619
- this.tokenManager.setUser(response.user);
620
- }
621
- if (response.csrfToken) {
622
- setCsrfToken(response.csrfToken);
623
- }
624
- return {
625
- data: { session: this.tokenManager.getSession() },
626
- error: null
627
- };
628
- }
629
- } catch (error) {
630
- if (error instanceof InsForgeError) {
631
- if (error.statusCode === 404) {
632
- this.tokenManager.setStorageMode();
633
- const session2 = this.tokenManager.getSession();
634
- if (session2) {
635
- return { data: { session: session2 }, error: null };
636
- }
637
- return { data: { session: null }, error: null };
638
- }
639
- return { data: { session: null }, error };
640
- }
752
+ const code = params.get("insforge_code");
753
+ if (code) {
754
+ cleanUrlParams("insforge_code");
755
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
756
+ if (exchangeError) {
757
+ console.debug("OAuth code exchange failed:", exchangeError.message);
641
758
  }
759
+ return;
642
760
  }
643
- return { data: { session: null }, error: null };
644
761
  } catch (error) {
645
- if (error instanceof InsForgeError) {
646
- return { data: { session: null }, error };
647
- }
648
- return {
649
- data: { session: null },
650
- error: new InsForgeError(
651
- "An unexpected error occurred while getting session",
652
- 500,
653
- "UNEXPECTED_ERROR"
654
- )
655
- };
762
+ console.debug("OAuth callback detection skipped:", error);
656
763
  }
657
764
  }
658
- /**
659
- * Set/Update the current user's profile
660
- * Updates profile information in the users table (supports any dynamic fields)
661
- * Requires authentication
662
- */
663
- async setProfile(profile) {
765
+ // ============================================================================
766
+ // Sign Up / Sign In / Sign Out
767
+ // ============================================================================
768
+ async signUp(request) {
664
769
  try {
665
- const response = await this.http.patch(
666
- "/api/auth/profiles/current",
667
- { profile }
770
+ const response = await this.http.post(
771
+ this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
772
+ request,
773
+ { credentials: "include" }
668
774
  );
669
- return {
670
- data: response,
671
- error: null
672
- };
673
- } catch (error) {
674
- if (error instanceof InsForgeError) {
675
- return { data: null, error };
775
+ if (response.accessToken && response.user) {
776
+ this.saveSessionFromResponse(response);
676
777
  }
677
- return {
678
- data: null,
679
- error: new InsForgeError(
680
- "An unexpected error occurred while updating user profile",
681
- 500,
682
- "UNEXPECTED_ERROR"
683
- )
684
- };
778
+ if (response.refreshToken) {
779
+ this.http.setRefreshToken(response.refreshToken);
780
+ }
781
+ return { data: response, error: null };
782
+ } catch (error) {
783
+ return wrapError(error, "An unexpected error occurred during sign up");
685
784
  }
686
785
  }
687
- /**
688
- * Send email verification (code or link based on config)
689
- *
690
- * Send email verification using the method configured in auth settings (verifyEmailMethod).
691
- * When method is 'code', sends a 6-digit numeric code. When method is 'link', sends a magic link.
692
- * Prevents user enumeration by returning success even if email doesn't exist.
693
- */
694
- async sendVerificationEmail(request) {
786
+ async signInWithPassword(request) {
695
787
  try {
696
788
  const response = await this.http.post(
697
- "/api/auth/email/send-verification",
698
- request
789
+ this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
790
+ request,
791
+ { credentials: "include" }
699
792
  );
700
- return {
701
- data: response,
702
- error: null
703
- };
793
+ this.saveSessionFromResponse(response);
794
+ if (response.refreshToken) {
795
+ this.http.setRefreshToken(response.refreshToken);
796
+ }
797
+ return { data: response, error: null };
704
798
  } catch (error) {
705
- if (error instanceof InsForgeError) {
706
- return { data: null, error };
799
+ return wrapError(error, "An unexpected error occurred during sign in");
800
+ }
801
+ }
802
+ async signOut() {
803
+ try {
804
+ try {
805
+ await this.http.post(
806
+ this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
807
+ void 0,
808
+ { credentials: "include" }
809
+ );
810
+ } catch {
707
811
  }
812
+ this.tokenManager.clearSession();
813
+ this.http.setAuthToken(null);
814
+ this.http.setRefreshToken(null);
815
+ if (!this.isServerMode()) {
816
+ clearCsrfToken();
817
+ }
818
+ return { error: null };
819
+ } catch {
708
820
  return {
709
- data: null,
710
- error: new InsForgeError(
711
- "An unexpected error occurred while sending verification code",
712
- 500,
713
- "UNEXPECTED_ERROR"
714
- )
821
+ error: new InsForgeError("Failed to sign out", 500, "SIGNOUT_ERROR")
715
822
  };
716
823
  }
717
824
  }
825
+ // ============================================================================
826
+ // OAuth Authentication
827
+ // ============================================================================
718
828
  /**
719
- * Send password reset (code or link based on config)
720
- *
721
- * Send password reset email using the method configured in auth settings (resetPasswordMethod).
722
- * When method is 'code', sends a 6-digit numeric code for two-step flow.
723
- * When method is 'link', sends a magic link.
724
- * Prevents user enumeration by returning success even if email doesn't exist.
829
+ * Sign in with OAuth provider using PKCE flow
725
830
  */
726
- async sendResetPasswordEmail(request) {
831
+ async signInWithOAuth(options) {
727
832
  try {
728
- const response = await this.http.post(
729
- "/api/auth/email/send-reset-password",
730
- request
833
+ const { provider, redirectTo, skipBrowserRedirect } = options;
834
+ const providerKey = encodeURIComponent(provider.toLowerCase());
835
+ const codeVerifier = generateCodeVerifier();
836
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
837
+ storePkceVerifier(codeVerifier);
838
+ const params = { code_challenge: codeChallenge };
839
+ if (redirectTo) params.redirect_uri = redirectTo;
840
+ const isBuiltInProvider = oAuthProvidersSchema.options.includes(
841
+ providerKey
731
842
  );
843
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
844
+ const response = await this.http.get(oauthPath, {
845
+ params
846
+ });
847
+ if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
848
+ window.location.href = response.authUrl;
849
+ return { data: {}, error: null };
850
+ }
732
851
  return {
733
- data: response,
852
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
734
853
  error: null
735
854
  };
736
855
  } catch (error) {
737
856
  if (error instanceof InsForgeError) {
738
- return { data: null, error };
857
+ return { data: {}, error };
739
858
  }
740
859
  return {
741
- data: null,
860
+ data: {},
742
861
  error: new InsForgeError(
743
- "An unexpected error occurred while sending password reset code",
862
+ "An unexpected error occurred during OAuth initialization",
744
863
  500,
745
864
  "UNEXPECTED_ERROR"
746
865
  )
@@ -748,123 +867,292 @@ var Auth = class {
748
867
  }
749
868
  }
750
869
  /**
751
- * Exchange reset password code for reset token
752
- *
753
- * Step 1 of two-step password reset flow (only used when resetPasswordMethod is 'code'):
754
- * 1. Verify the 6-digit code sent to user's email
755
- * 2. Return a reset token that can be used to actually reset the password
756
- *
757
- * This endpoint is not used when resetPasswordMethod is 'link' (magic link flow is direct).
870
+ * Exchange OAuth authorization code for tokens (PKCE flow)
871
+ * Called automatically on initialization when insforge_code is in URL
758
872
  */
759
- async exchangeResetPasswordToken(request) {
873
+ async exchangeOAuthCode(code, codeVerifier) {
760
874
  try {
875
+ const verifier = codeVerifier ?? retrievePkceVerifier();
876
+ if (!verifier) {
877
+ return {
878
+ data: null,
879
+ error: new InsForgeError(
880
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
881
+ 400,
882
+ "PKCE_VERIFIER_MISSING"
883
+ )
884
+ };
885
+ }
886
+ const request = {
887
+ code,
888
+ code_verifier: verifier
889
+ };
761
890
  const response = await this.http.post(
762
- "/api/auth/email/exchange-reset-password-token",
763
- request
891
+ this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
892
+ request,
893
+ { credentials: "include" }
764
894
  );
895
+ this.saveSessionFromResponse(response);
765
896
  return {
766
897
  data: response,
767
898
  error: null
768
899
  };
769
900
  } catch (error) {
770
- if (error instanceof InsForgeError) {
771
- return { data: null, error };
772
- }
773
- return {
774
- data: null,
775
- error: new InsForgeError(
776
- "An unexpected error occurred while verifying reset code",
777
- 500,
778
- "UNEXPECTED_ERROR"
779
- )
780
- };
901
+ return wrapError(
902
+ error,
903
+ "An unexpected error occurred during OAuth code exchange"
904
+ );
781
905
  }
782
906
  }
783
907
  /**
784
- * Reset password with token
785
- *
786
- * Reset user password with a token. The token can be:
787
- * - Magic link token (64-character hex token from send-reset-password when method is 'link')
788
- * - Reset token (from exchange-reset-password-token after code verification when method is 'code')
789
- *
790
- * Both token types use RESET_PASSWORD purpose and are verified the same way.
908
+ * Sign in with an ID token from a native SDK (Google One Tap, etc.)
909
+ * Use this for native mobile apps or Google One Tap on web.
791
910
  *
792
- * Flow summary:
793
- * - Code method: send-reset-password exchange-reset-password-token reset-password (with resetToken)
794
- * - Link method: send-reset-password → reset-password (with link token directly)
911
+ * @param credentials.provider - The identity provider (currently only 'google' is supported)
912
+ * @param credentials.token - The ID token from the native SDK
795
913
  */
796
- async resetPassword(request) {
914
+ async signInWithIdToken(credentials) {
797
915
  try {
916
+ const { provider, token } = credentials;
798
917
  const response = await this.http.post(
799
- "/api/auth/email/reset-password",
800
- request
918
+ "/api/auth/id-token?client_type=mobile",
919
+ { provider, token },
920
+ { credentials: "include" }
801
921
  );
922
+ this.saveSessionFromResponse(response);
923
+ if (response.refreshToken) {
924
+ this.http.setRefreshToken(response.refreshToken);
925
+ }
802
926
  return {
803
927
  data: response,
804
928
  error: null
805
929
  };
806
930
  } catch (error) {
807
- if (error instanceof InsForgeError) {
808
- return { data: null, error };
809
- }
810
- return {
811
- data: null,
812
- error: new InsForgeError(
813
- "An unexpected error occurred while resetting password",
814
- 500,
815
- "UNEXPECTED_ERROR"
816
- )
817
- };
931
+ return wrapError(
932
+ error,
933
+ "An unexpected error occurred during ID token sign in"
934
+ );
818
935
  }
819
936
  }
937
+ // ============================================================================
938
+ // Session Management
939
+ // ============================================================================
820
940
  /**
821
- * Verify email with code or link
941
+ * Refresh the current auth session.
822
942
  *
823
- * Verify email address using the method configured in auth settings (verifyEmailMethod):
824
- * - Code verification: Provide both `email` and `otp` (6-digit numeric code)
825
- * - Link verification: Provide only `otp` (64-character hex token from magic link)
943
+ * Browser mode:
944
+ * - Uses httpOnly refresh cookie and optional CSRF header.
826
945
  *
827
- * Successfully verified users will receive a session token.
828
- *
829
- * The email verification link sent to users always points to the backend API endpoint.
830
- * If `verifyEmailRedirectTo` is configured, the backend will redirect to that URL after successful verification.
831
- * Otherwise, a default success page is displayed.
946
+ * Server mode (`isServerMode: true`):
947
+ * - Uses mobile auth flow and requires `refreshToken` in request body.
832
948
  */
833
- async verifyEmail(request) {
949
+ async refreshSession(options) {
834
950
  try {
951
+ if (this.isServerMode() && !options?.refreshToken) {
952
+ return {
953
+ data: null,
954
+ error: new InsForgeError(
955
+ "refreshToken is required when refreshing session in server mode",
956
+ 400,
957
+ "REFRESH_TOKEN_REQUIRED"
958
+ )
959
+ };
960
+ }
961
+ const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
835
962
  const response = await this.http.post(
836
- "/api/auth/email/verify",
837
- request
963
+ this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
964
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
965
+ {
966
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
967
+ credentials: "include"
968
+ }
838
969
  );
839
- if (!isHostedAuthEnvironment()) {
840
- const session = {
841
- accessToken: response.accessToken,
842
- user: response.user
843
- };
844
- this.tokenManager.saveSession(session);
845
- this.http.setAuthToken(response.accessToken);
846
- if (response.csrfToken) {
847
- setCsrfToken(response.csrfToken);
970
+ if (response.accessToken) {
971
+ this.saveSessionFromResponse(response);
972
+ }
973
+ return { data: response, error: null };
974
+ } catch (error) {
975
+ return wrapError(
976
+ error,
977
+ "An unexpected error occurred during session refresh"
978
+ );
979
+ }
980
+ }
981
+ /**
982
+ * Get current user, automatically waits for pending OAuth callback
983
+ */
984
+ async getCurrentUser() {
985
+ await this.authCallbackHandled;
986
+ try {
987
+ if (this.isServerMode()) {
988
+ const accessToken = this.tokenManager.getAccessToken();
989
+ if (!accessToken) return { data: { user: null }, error: null };
990
+ this.http.setAuthToken(accessToken);
991
+ const response = await this.http.get(
992
+ "/api/auth/sessions/current"
993
+ );
994
+ const user = response.user ?? null;
995
+ return { data: { user }, error: null };
996
+ }
997
+ const session = this.tokenManager.getSession();
998
+ if (session) {
999
+ this.http.setAuthToken(session.accessToken);
1000
+ return { data: { user: session.user }, error: null };
1001
+ }
1002
+ if (typeof window !== "undefined") {
1003
+ const { data: refreshed, error: refreshError } = await this.refreshSession();
1004
+ if (refreshError) {
1005
+ return { data: { user: null }, error: refreshError };
1006
+ }
1007
+ if (refreshed?.accessToken) {
1008
+ return { data: { user: refreshed.user ?? null }, error: null };
848
1009
  }
849
1010
  }
850
- return {
851
- data: response,
852
- error: null
853
- };
1011
+ return { data: { user: null }, error: null };
854
1012
  } catch (error) {
855
1013
  if (error instanceof InsForgeError) {
856
- return { data: null, error };
1014
+ return { data: { user: null }, error };
857
1015
  }
858
1016
  return {
859
- data: null,
1017
+ data: { user: null },
860
1018
  error: new InsForgeError(
861
- "An unexpected error occurred while verifying email",
1019
+ "An unexpected error occurred while getting user",
862
1020
  500,
863
1021
  "UNEXPECTED_ERROR"
864
1022
  )
865
1023
  };
866
1024
  }
867
1025
  }
1026
+ // ============================================================================
1027
+ // Profile Management
1028
+ // ============================================================================
1029
+ async getProfile(userId) {
1030
+ try {
1031
+ const response = await this.http.get(
1032
+ `/api/auth/profiles/${userId}`
1033
+ );
1034
+ return { data: response, error: null };
1035
+ } catch (error) {
1036
+ return wrapError(
1037
+ error,
1038
+ "An unexpected error occurred while fetching user profile"
1039
+ );
1040
+ }
1041
+ }
1042
+ async setProfile(profile) {
1043
+ try {
1044
+ const response = await this.http.patch(
1045
+ "/api/auth/profiles/current",
1046
+ {
1047
+ profile
1048
+ }
1049
+ );
1050
+ const currentUser = this.tokenManager.getUser();
1051
+ if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1052
+ this.tokenManager.setUser({
1053
+ ...currentUser,
1054
+ profile: response.profile
1055
+ });
1056
+ }
1057
+ return { data: response, error: null };
1058
+ } catch (error) {
1059
+ return wrapError(
1060
+ error,
1061
+ "An unexpected error occurred while updating user profile"
1062
+ );
1063
+ }
1064
+ }
1065
+ // ============================================================================
1066
+ // Email Verification
1067
+ // ============================================================================
1068
+ async resendVerificationEmail(request) {
1069
+ try {
1070
+ const response = await this.http.post("/api/auth/email/send-verification", request);
1071
+ return { data: response, error: null };
1072
+ } catch (error) {
1073
+ return wrapError(
1074
+ error,
1075
+ "An unexpected error occurred while sending verification email"
1076
+ );
1077
+ }
1078
+ }
1079
+ async verifyEmail(request) {
1080
+ try {
1081
+ const response = await this.http.post(
1082
+ this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1083
+ request,
1084
+ { credentials: "include" }
1085
+ );
1086
+ this.saveSessionFromResponse(response);
1087
+ if (response.refreshToken) {
1088
+ this.http.setRefreshToken(response.refreshToken);
1089
+ }
1090
+ return { data: response, error: null };
1091
+ } catch (error) {
1092
+ return wrapError(
1093
+ error,
1094
+ "An unexpected error occurred while verifying email"
1095
+ );
1096
+ }
1097
+ }
1098
+ // ============================================================================
1099
+ // Password Reset
1100
+ // ============================================================================
1101
+ async sendResetPasswordEmail(request) {
1102
+ try {
1103
+ const response = await this.http.post("/api/auth/email/send-reset-password", request);
1104
+ return { data: response, error: null };
1105
+ } catch (error) {
1106
+ return wrapError(
1107
+ error,
1108
+ "An unexpected error occurred while sending password reset email"
1109
+ );
1110
+ }
1111
+ }
1112
+ async exchangeResetPasswordToken(request) {
1113
+ try {
1114
+ const response = await this.http.post(
1115
+ "/api/auth/email/exchange-reset-password-token",
1116
+ request
1117
+ );
1118
+ return { data: response, error: null };
1119
+ } catch (error) {
1120
+ return wrapError(
1121
+ error,
1122
+ "An unexpected error occurred while verifying reset code"
1123
+ );
1124
+ }
1125
+ }
1126
+ async resetPassword(request) {
1127
+ try {
1128
+ const response = await this.http.post(
1129
+ "/api/auth/email/reset-password",
1130
+ request
1131
+ );
1132
+ return { data: response, error: null };
1133
+ } catch (error) {
1134
+ return wrapError(
1135
+ error,
1136
+ "An unexpected error occurred while resetting password"
1137
+ );
1138
+ }
1139
+ }
1140
+ // ============================================================================
1141
+ // Configuration
1142
+ // ============================================================================
1143
+ async getPublicAuthConfig() {
1144
+ try {
1145
+ const response = await this.http.get(
1146
+ "/api/auth/public-config"
1147
+ );
1148
+ return { data: response, error: null };
1149
+ } catch (error) {
1150
+ return wrapError(
1151
+ error,
1152
+ "An unexpected error occurred while fetching auth configuration"
1153
+ );
1154
+ }
1155
+ }
868
1156
  };
869
1157
 
870
1158
  // src/modules/database-postgrest.ts
@@ -873,8 +1161,10 @@ function createInsForgePostgrestFetch(httpClient, tokenManager) {
873
1161
  return async (input, init) => {
874
1162
  const url = typeof input === "string" ? input : input.toString();
875
1163
  const urlObj = new URL(url);
876
- const tableName = urlObj.pathname.slice(1);
877
- const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
1164
+ const pathname = urlObj.pathname.slice(1);
1165
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1166
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1167
+ const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
878
1168
  const token = tokenManager.getAccessToken();
879
1169
  const httpHeaders = httpClient.getHeaders();
880
1170
  const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
@@ -934,6 +1224,25 @@ var Database = class {
934
1224
  from(table) {
935
1225
  return this.postgrest.from(table);
936
1226
  }
1227
+ /**
1228
+ * Call a PostgreSQL function (RPC)
1229
+ *
1230
+ * @example
1231
+ * // Call a function with parameters
1232
+ * const { data, error } = await client.database
1233
+ * .rpc('get_user_stats', { user_id: 123 });
1234
+ *
1235
+ * // Call a function with no parameters
1236
+ * const { data, error } = await client.database
1237
+ * .rpc('get_all_active_users');
1238
+ *
1239
+ * // With options (head, count, get)
1240
+ * const { data, count } = await client.database
1241
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1242
+ */
1243
+ rpc(fn, args, options) {
1244
+ return this.postgrest.rpc(fn, args, options);
1245
+ }
937
1246
  };
938
1247
 
939
1248
  // src/modules/storage.ts
@@ -1218,6 +1527,7 @@ var AI = class {
1218
1527
  this.http = http;
1219
1528
  this.chat = new Chat(http);
1220
1529
  this.images = new Images(http);
1530
+ this.embeddings = new Embeddings(http);
1221
1531
  }
1222
1532
  };
1223
1533
  var Chat = class {
@@ -1241,16 +1551,46 @@ var ChatCompletions = class {
1241
1551
  * });
1242
1552
  * console.log(completion.choices[0].message.content);
1243
1553
  *
1244
- * // With images
1554
+ * // With images (OpenAI-compatible format)
1245
1555
  * const response = await client.ai.chat.completions.create({
1246
1556
  * model: 'gpt-4-vision',
1247
1557
  * messages: [{
1248
1558
  * role: 'user',
1249
- * content: 'What is in this image?',
1250
- * images: [{ url: 'https://example.com/image.jpg' }]
1559
+ * content: [
1560
+ * { type: 'text', text: 'What is in this image?' },
1561
+ * { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
1562
+ * ]
1251
1563
  * }]
1252
1564
  * });
1253
1565
  *
1566
+ * // With PDF files
1567
+ * const pdfResponse = await client.ai.chat.completions.create({
1568
+ * model: 'anthropic/claude-3.5-sonnet',
1569
+ * messages: [{
1570
+ * role: 'user',
1571
+ * content: [
1572
+ * { type: 'text', text: 'Summarize this document' },
1573
+ * { type: 'file', file: { filename: 'doc.pdf', file_data: 'https://example.com/doc.pdf' } }
1574
+ * ]
1575
+ * }],
1576
+ * fileParser: { enabled: true, pdf: { engine: 'mistral-ocr' } }
1577
+ * });
1578
+ *
1579
+ * // With web search
1580
+ * const searchResponse = await client.ai.chat.completions.create({
1581
+ * model: 'openai/gpt-4',
1582
+ * messages: [{ role: 'user', content: 'What are the latest news about AI?' }],
1583
+ * webSearch: { enabled: true, maxResults: 5 }
1584
+ * });
1585
+ * // Access citations from response.choices[0].message.annotations
1586
+ *
1587
+ * // With thinking/reasoning mode (Anthropic models)
1588
+ * const thinkingResponse = await client.ai.chat.completions.create({
1589
+ * model: 'anthropic/claude-3.5-sonnet',
1590
+ * messages: [{ role: 'user', content: 'Solve this complex math problem...' }],
1591
+ * thinking: true
1592
+ * });
1593
+ *
1254
1594
  * // Streaming - returns async iterable
1255
1595
  * const stream = await client.ai.chat.completions.create({
1256
1596
  * model: 'gpt-4',
@@ -1272,7 +1612,15 @@ var ChatCompletions = class {
1272
1612
  temperature: params.temperature,
1273
1613
  maxTokens: params.maxTokens,
1274
1614
  topP: params.topP,
1275
- stream: params.stream
1615
+ stream: params.stream,
1616
+ // New plugin options
1617
+ webSearch: params.webSearch,
1618
+ fileParser: params.fileParser,
1619
+ thinking: params.thinking,
1620
+ // Tool calling options
1621
+ tools: params.tools,
1622
+ toolChoice: params.toolChoice,
1623
+ parallelToolCalls: params.parallelToolCalls
1276
1624
  };
1277
1625
  if (params.stream) {
1278
1626
  const headers = this.http.getHeaders();
@@ -1306,9 +1654,13 @@ var ChatCompletions = class {
1306
1654
  index: 0,
1307
1655
  message: {
1308
1656
  role: "assistant",
1309
- content
1657
+ content,
1658
+ // Include tool_calls if present (from tool calling)
1659
+ ...response.tool_calls?.length && { tool_calls: response.tool_calls },
1660
+ // Include annotations if present (from web search or file parsing)
1661
+ ...response.annotations?.length && { annotations: response.annotations }
1310
1662
  },
1311
- finish_reason: "stop"
1663
+ finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
1312
1664
  }
1313
1665
  ],
1314
1666
  usage: response.metadata?.usage || {
@@ -1350,7 +1702,24 @@ var ChatCompletions = class {
1350
1702
  delta: {
1351
1703
  content: data.chunk || data.content
1352
1704
  },
1353
- finish_reason: data.done ? "stop" : null
1705
+ finish_reason: null
1706
+ }
1707
+ ]
1708
+ };
1709
+ }
1710
+ if (data.tool_calls?.length) {
1711
+ yield {
1712
+ id: `chatcmpl-${Date.now()}`,
1713
+ object: "chat.completion.chunk",
1714
+ created: Math.floor(Date.now() / 1e3),
1715
+ model,
1716
+ choices: [
1717
+ {
1718
+ index: 0,
1719
+ delta: {
1720
+ tool_calls: data.tool_calls
1721
+ },
1722
+ finish_reason: "tool_calls"
1354
1723
  }
1355
1724
  ]
1356
1725
  };
@@ -1371,6 +1740,65 @@ var ChatCompletions = class {
1371
1740
  }
1372
1741
  }
1373
1742
  };
1743
+ var Embeddings = class {
1744
+ constructor(http) {
1745
+ this.http = http;
1746
+ }
1747
+ /**
1748
+ * Create embeddings for text input - OpenAI-like response format
1749
+ *
1750
+ * @example
1751
+ * ```typescript
1752
+ * // Single text input
1753
+ * const response = await client.ai.embeddings.create({
1754
+ * model: 'openai/text-embedding-3-small',
1755
+ * input: 'Hello world'
1756
+ * });
1757
+ * console.log(response.data[0].embedding); // number[]
1758
+ *
1759
+ * // Multiple text inputs
1760
+ * const response = await client.ai.embeddings.create({
1761
+ * model: 'openai/text-embedding-3-small',
1762
+ * input: ['Hello world', 'Goodbye world']
1763
+ * });
1764
+ * response.data.forEach((item, i) => {
1765
+ * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
1766
+ * });
1767
+ *
1768
+ * // With custom dimensions (if supported by model)
1769
+ * const response = await client.ai.embeddings.create({
1770
+ * model: 'openai/text-embedding-3-small',
1771
+ * input: 'Hello world',
1772
+ * dimensions: 256
1773
+ * });
1774
+ *
1775
+ * // With base64 encoding format
1776
+ * const response = await client.ai.embeddings.create({
1777
+ * model: 'openai/text-embedding-3-small',
1778
+ * input: 'Hello world',
1779
+ * encoding_format: 'base64'
1780
+ * });
1781
+ * ```
1782
+ */
1783
+ async create(params) {
1784
+ const response = await this.http.post(
1785
+ "/api/ai/embeddings",
1786
+ params
1787
+ );
1788
+ return {
1789
+ object: response.object,
1790
+ data: response.data,
1791
+ model: response.metadata?.model,
1792
+ usage: response.metadata?.usage ? {
1793
+ prompt_tokens: response.metadata.usage.promptTokens || 0,
1794
+ total_tokens: response.metadata.usage.totalTokens || 0
1795
+ } : {
1796
+ prompt_tokens: 0,
1797
+ total_tokens: 0
1798
+ }
1799
+ };
1800
+ }
1801
+ };
1374
1802
  var Images = class {
1375
1803
  constructor(http) {
1376
1804
  this.http = http;
@@ -1428,30 +1856,73 @@ var Images = class {
1428
1856
  };
1429
1857
 
1430
1858
  // src/modules/functions.ts
1431
- var Functions = class {
1432
- constructor(http) {
1859
+ var Functions = class _Functions {
1860
+ constructor(http, functionsUrl) {
1433
1861
  this.http = http;
1862
+ this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
1863
+ }
1864
+ /**
1865
+ * Derive the subhosting URL from the base URL.
1866
+ * Base URL pattern: https://{appKey}.{region}.insforge.app
1867
+ * Functions URL: https://{appKey}.functions.insforge.app
1868
+ * Only applies to .insforge.app domains.
1869
+ */
1870
+ static deriveSubhostingUrl(baseUrl) {
1871
+ try {
1872
+ const { hostname } = new URL(baseUrl);
1873
+ if (!hostname.endsWith(".insforge.app")) return void 0;
1874
+ const appKey = hostname.split(".")[0];
1875
+ return `https://${appKey}.functions.insforge.app`;
1876
+ } catch {
1877
+ return void 0;
1878
+ }
1434
1879
  }
1435
1880
  /**
1436
1881
  * Invokes an Edge Function
1882
+ *
1883
+ * If functionsUrl is configured, tries direct subhosting first.
1884
+ * Falls back to proxy URL if subhosting returns 404.
1885
+ *
1437
1886
  * @param slug The function slug to invoke
1438
1887
  * @param options Request options
1439
1888
  */
1440
1889
  async invoke(slug, options = {}) {
1890
+ const { method = "POST", body, headers = {} } = options;
1891
+ if (this.functionsUrl) {
1892
+ try {
1893
+ const data = await this.http.request(method, `${this.functionsUrl}/${slug}`, {
1894
+ body,
1895
+ headers
1896
+ });
1897
+ return { data, error: null };
1898
+ } catch (error) {
1899
+ if (error instanceof Error && error.name === "AbortError") throw error;
1900
+ if (error instanceof InsForgeError && error.statusCode === 404) {
1901
+ } else {
1902
+ return {
1903
+ data: null,
1904
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1905
+ error instanceof Error ? error.message : "Function invocation failed",
1906
+ 500,
1907
+ "FUNCTION_ERROR"
1908
+ )
1909
+ };
1910
+ }
1911
+ }
1912
+ }
1441
1913
  try {
1442
- const { method = "POST", body, headers = {} } = options;
1443
1914
  const path = `/functions/${slug}`;
1444
- const data = await this.http.request(
1445
- method,
1446
- path,
1447
- { body, headers }
1448
- );
1915
+ const data = await this.http.request(method, path, { body, headers });
1449
1916
  return { data, error: null };
1450
1917
  } catch (error) {
1918
+ if (error instanceof Error && error.name === "AbortError") throw error;
1451
1919
  return {
1452
1920
  data: null,
1453
- error
1454
- // Pass through the full error object with all properties
1921
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1922
+ error instanceof Error ? error.message : "Function invocation failed",
1923
+ 500,
1924
+ "FUNCTION_ERROR"
1925
+ )
1455
1926
  };
1456
1927
  }
1457
1928
  }
@@ -1461,13 +1932,15 @@ var Functions = class {
1461
1932
  import { io } from "socket.io-client";
1462
1933
  var CONNECT_TIMEOUT = 1e4;
1463
1934
  var Realtime = class {
1464
- constructor(baseUrl, tokenManager) {
1935
+ constructor(baseUrl, tokenManager, anonKey) {
1465
1936
  this.socket = null;
1466
1937
  this.connectPromise = null;
1467
1938
  this.subscribedChannels = /* @__PURE__ */ new Set();
1468
1939
  this.eventListeners = /* @__PURE__ */ new Map();
1469
1940
  this.baseUrl = baseUrl;
1470
1941
  this.tokenManager = tokenManager;
1942
+ this.anonKey = anonKey;
1943
+ this.tokenManager.onTokenChange = () => this.onTokenChange();
1471
1944
  }
1472
1945
  notifyListeners(event, payload) {
1473
1946
  const listeners = this.eventListeners.get(event);
@@ -1492,8 +1965,7 @@ var Realtime = class {
1492
1965
  return this.connectPromise;
1493
1966
  }
1494
1967
  this.connectPromise = new Promise((resolve, reject) => {
1495
- const session = this.tokenManager.getSession();
1496
- const token = session?.accessToken;
1968
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
1497
1969
  this.socket = io(this.baseUrl, {
1498
1970
  transports: ["websocket"],
1499
1971
  auth: token ? { token } : void 0
@@ -1559,6 +2031,21 @@ var Realtime = class {
1559
2031
  }
1560
2032
  this.subscribedChannels.clear();
1561
2033
  }
2034
+ /**
2035
+ * Handle token changes (e.g., after auth refresh)
2036
+ * Updates socket auth so reconnects use the new token
2037
+ * If connected, triggers reconnect to apply new token immediately
2038
+ */
2039
+ onTokenChange() {
2040
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2041
+ if (this.socket) {
2042
+ this.socket.auth = token ? { token } : {};
2043
+ }
2044
+ if (this.socket && (this.socket.connected || this.connectPromise)) {
2045
+ this.socket.disconnect();
2046
+ this.socket.connect();
2047
+ }
2048
+ }
1562
2049
  /**
1563
2050
  * Check if connected to the realtime server
1564
2051
  */
@@ -1707,8 +2194,15 @@ var Emails = class {
1707
2194
  );
1708
2195
  return { data, error: null };
1709
2196
  } catch (error) {
1710
- const normalizedError = error instanceof Error ? error : new Error(String(error));
1711
- return { data: null, error: normalizedError };
2197
+ if (error instanceof Error && error.name === "AbortError") throw error;
2198
+ return {
2199
+ data: null,
2200
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2201
+ error instanceof Error ? error.message : "Email send failed",
2202
+ 500,
2203
+ "EMAIL_ERROR"
2204
+ )
2205
+ };
1712
2206
  }
1713
2207
  }
1714
2208
  };
@@ -1716,31 +2210,30 @@ var Emails = class {
1716
2210
  // src/client.ts
1717
2211
  var InsForgeClient = class {
1718
2212
  constructor(config = {}) {
1719
- this.http = new HttpClient(config);
1720
- this.tokenManager = new TokenManager(config.storage);
2213
+ const logger = new Logger(config.debug);
2214
+ this.tokenManager = new TokenManager();
2215
+ this.http = new HttpClient(config, this.tokenManager, logger);
1721
2216
  if (config.edgeFunctionToken) {
1722
2217
  this.http.setAuthToken(config.edgeFunctionToken);
1723
- this.tokenManager.saveSession({
1724
- accessToken: config.edgeFunctionToken,
1725
- user: {}
1726
- // Will be populated by getCurrentUser()
1727
- });
1728
- }
1729
- const existingSession = this.tokenManager.getSession();
1730
- if (existingSession?.accessToken) {
1731
- this.http.setAuthToken(existingSession.accessToken);
2218
+ this.tokenManager.setAccessToken(config.edgeFunctionToken);
1732
2219
  }
1733
- this.auth = new Auth(this.http, this.tokenManager);
2220
+ this.auth = new Auth(this.http, this.tokenManager, {
2221
+ isServerMode: config.isServerMode ?? false
2222
+ });
1734
2223
  this.database = new Database(this.http, this.tokenManager);
1735
2224
  this.storage = new Storage(this.http);
1736
2225
  this.ai = new AI(this.http);
1737
- this.functions = new Functions(this.http);
1738
- this.realtime = new Realtime(this.http.baseUrl, this.tokenManager);
2226
+ this.functions = new Functions(this.http, config.functionsUrl);
2227
+ this.realtime = new Realtime(
2228
+ this.http.baseUrl,
2229
+ this.tokenManager,
2230
+ config.anonKey
2231
+ );
1739
2232
  this.emails = new Emails(this.http);
1740
2233
  }
1741
2234
  /**
1742
2235
  * Get the underlying HTTP client for custom requests
1743
- *
2236
+ *
1744
2237
  * @example
1745
2238
  * ```typescript
1746
2239
  * const httpClient = client.getHttpClient();
@@ -1774,6 +2267,7 @@ export {
1774
2267
  HttpClient,
1775
2268
  InsForgeClient,
1776
2269
  InsForgeError,
2270
+ Logger,
1777
2271
  Realtime,
1778
2272
  Storage,
1779
2273
  StorageBucket,