@pawabase/client 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +313 -2
  2. package/dist/cjs/admin.d.ts +187 -0
  3. package/dist/cjs/admin.d.ts.map +1 -0
  4. package/dist/cjs/admin.js +235 -0
  5. package/dist/cjs/admin.js.map +1 -0
  6. package/dist/cjs/auth.d.ts +448 -0
  7. package/dist/cjs/auth.d.ts.map +1 -0
  8. package/dist/cjs/auth.js +1016 -0
  9. package/dist/cjs/auth.js.map +1 -0
  10. package/dist/cjs/client.d.ts +103 -0
  11. package/dist/cjs/client.d.ts.map +1 -0
  12. package/dist/cjs/client.js +144 -0
  13. package/dist/cjs/client.js.map +1 -0
  14. package/dist/cjs/data.d.ts +220 -0
  15. package/dist/cjs/data.d.ts.map +1 -0
  16. package/dist/cjs/data.js +490 -0
  17. package/dist/cjs/data.js.map +1 -0
  18. package/dist/cjs/errors.d.ts +167 -0
  19. package/dist/cjs/errors.d.ts.map +1 -0
  20. package/dist/cjs/errors.js +316 -0
  21. package/dist/cjs/errors.js.map +1 -0
  22. package/dist/cjs/files.d.ts +80 -0
  23. package/dist/cjs/files.d.ts.map +1 -0
  24. package/dist/cjs/files.js +167 -0
  25. package/dist/cjs/files.js.map +1 -0
  26. package/dist/cjs/functions.d.ts +66 -0
  27. package/dist/cjs/functions.d.ts.map +1 -0
  28. package/dist/cjs/functions.js +109 -0
  29. package/dist/cjs/functions.js.map +1 -0
  30. package/dist/cjs/http.d.ts +101 -0
  31. package/dist/cjs/http.d.ts.map +1 -0
  32. package/dist/cjs/http.js +249 -0
  33. package/dist/cjs/http.js.map +1 -0
  34. package/dist/cjs/index.d.ts +22 -0
  35. package/dist/cjs/index.d.ts.map +1 -0
  36. package/dist/cjs/index.js +45 -0
  37. package/dist/cjs/index.js.map +1 -0
  38. package/dist/cjs/manage/access.d.ts +136 -0
  39. package/dist/cjs/manage/access.d.ts.map +1 -0
  40. package/dist/cjs/manage/access.js +162 -0
  41. package/dist/cjs/manage/access.js.map +1 -0
  42. package/dist/cjs/manage/automation.d.ts +106 -0
  43. package/dist/cjs/manage/automation.d.ts.map +1 -0
  44. package/dist/cjs/manage/automation.js +144 -0
  45. package/dist/cjs/manage/automation.js.map +1 -0
  46. package/dist/cjs/manage/base.d.ts +52 -0
  47. package/dist/cjs/manage/base.d.ts.map +1 -0
  48. package/dist/cjs/manage/base.js +69 -0
  49. package/dist/cjs/manage/base.js.map +1 -0
  50. package/dist/cjs/manage/data.d.ts +100 -0
  51. package/dist/cjs/manage/data.d.ts.map +1 -0
  52. package/dist/cjs/manage/data.js +141 -0
  53. package/dist/cjs/manage/data.js.map +1 -0
  54. package/dist/cjs/manage/files.d.ts +45 -0
  55. package/dist/cjs/manage/files.d.ts.map +1 -0
  56. package/dist/cjs/manage/files.js +73 -0
  57. package/dist/cjs/manage/files.js.map +1 -0
  58. package/dist/cjs/manage/functions.d.ts +48 -0
  59. package/dist/cjs/manage/functions.d.ts.map +1 -0
  60. package/dist/cjs/manage/functions.js +61 -0
  61. package/dist/cjs/manage/functions.js.map +1 -0
  62. package/dist/cjs/manage/index.d.ts +76 -0
  63. package/dist/cjs/manage/index.d.ts.map +1 -0
  64. package/dist/cjs/manage/index.js +134 -0
  65. package/dist/cjs/manage/index.js.map +1 -0
  66. package/dist/cjs/manage/lifecycle.d.ts +128 -0
  67. package/dist/cjs/manage/lifecycle.d.ts.map +1 -0
  68. package/dist/cjs/manage/lifecycle.js +167 -0
  69. package/dist/cjs/manage/lifecycle.js.map +1 -0
  70. package/dist/cjs/manage/mail.d.ts +16 -0
  71. package/dist/cjs/manage/mail.d.ts.map +1 -0
  72. package/dist/cjs/manage/mail.js +24 -0
  73. package/dist/cjs/manage/mail.js.map +1 -0
  74. package/dist/cjs/manage/observe.d.ts +149 -0
  75. package/dist/cjs/manage/observe.d.ts.map +1 -0
  76. package/dist/cjs/manage/observe.js +168 -0
  77. package/dist/cjs/manage/observe.js.map +1 -0
  78. package/dist/cjs/manage/realtime.d.ts +52 -0
  79. package/dist/cjs/manage/realtime.d.ts.map +1 -0
  80. package/dist/cjs/manage/realtime.js +71 -0
  81. package/dist/cjs/manage/realtime.js.map +1 -0
  82. package/dist/cjs/package.json +1 -0
  83. package/dist/cjs/realtime.d.ts +207 -0
  84. package/dist/cjs/realtime.d.ts.map +1 -0
  85. package/dist/cjs/realtime.js +704 -0
  86. package/dist/cjs/realtime.js.map +1 -0
  87. package/dist/cjs/session-store.d.ts +16 -0
  88. package/dist/cjs/session-store.d.ts.map +1 -0
  89. package/dist/cjs/session-store.js +37 -0
  90. package/dist/cjs/session-store.js.map +1 -0
  91. package/dist/cjs/typegen.d.ts +26 -0
  92. package/dist/cjs/typegen.d.ts.map +1 -0
  93. package/dist/cjs/typegen.js +167 -0
  94. package/dist/cjs/typegen.js.map +1 -0
  95. package/dist/cjs/types.d.ts +178 -0
  96. package/dist/cjs/types.d.ts.map +1 -0
  97. package/dist/cjs/types.js +4 -0
  98. package/dist/cjs/types.js.map +1 -0
  99. package/dist/cjs/util.d.ts +27 -0
  100. package/dist/cjs/util.d.ts.map +1 -0
  101. package/dist/cjs/util.js +115 -0
  102. package/dist/cjs/util.js.map +1 -0
  103. package/dist/cjs/version.d.ts +2 -0
  104. package/dist/cjs/version.d.ts.map +1 -0
  105. package/dist/cjs/version.js +5 -0
  106. package/dist/cjs/version.js.map +1 -0
  107. package/dist/esm/admin.d.ts +187 -0
  108. package/dist/esm/admin.d.ts.map +1 -0
  109. package/dist/esm/admin.js +222 -0
  110. package/dist/esm/admin.js.map +1 -0
  111. package/dist/esm/auth.d.ts +448 -0
  112. package/dist/esm/auth.d.ts.map +1 -0
  113. package/dist/esm/auth.js +1012 -0
  114. package/dist/esm/auth.js.map +1 -0
  115. package/dist/esm/cli/gen-types.d.ts +2 -0
  116. package/dist/esm/cli/gen-types.d.ts.map +1 -0
  117. package/dist/esm/cli/gen-types.js +100 -0
  118. package/dist/esm/cli/gen-types.js.map +1 -0
  119. package/dist/esm/client.d.ts +103 -0
  120. package/dist/esm/client.d.ts.map +1 -0
  121. package/dist/esm/client.js +139 -0
  122. package/dist/esm/client.js.map +1 -0
  123. package/dist/esm/data.d.ts +220 -0
  124. package/dist/esm/data.d.ts.map +1 -0
  125. package/dist/esm/data.js +485 -0
  126. package/dist/esm/data.js.map +1 -0
  127. package/dist/esm/errors.d.ts +167 -0
  128. package/dist/esm/errors.d.ts.map +1 -0
  129. package/dist/esm/errors.js +293 -0
  130. package/dist/esm/errors.js.map +1 -0
  131. package/dist/esm/files.d.ts +80 -0
  132. package/dist/esm/files.d.ts.map +1 -0
  133. package/dist/esm/files.js +162 -0
  134. package/dist/esm/files.js.map +1 -0
  135. package/dist/esm/functions.d.ts +66 -0
  136. package/dist/esm/functions.d.ts.map +1 -0
  137. package/dist/esm/functions.js +103 -0
  138. package/dist/esm/functions.js.map +1 -0
  139. package/dist/esm/http.d.ts +101 -0
  140. package/dist/esm/http.d.ts.map +1 -0
  141. package/dist/esm/http.js +245 -0
  142. package/dist/esm/http.js.map +1 -0
  143. package/dist/esm/index.d.ts +22 -0
  144. package/dist/esm/index.d.ts.map +1 -0
  145. package/dist/esm/index.js +12 -0
  146. package/dist/esm/index.js.map +1 -0
  147. package/dist/esm/manage/access.d.ts +136 -0
  148. package/dist/esm/manage/access.d.ts.map +1 -0
  149. package/dist/esm/manage/access.js +155 -0
  150. package/dist/esm/manage/access.js.map +1 -0
  151. package/dist/esm/manage/automation.d.ts +106 -0
  152. package/dist/esm/manage/automation.d.ts.map +1 -0
  153. package/dist/esm/manage/automation.js +134 -0
  154. package/dist/esm/manage/automation.js.map +1 -0
  155. package/dist/esm/manage/base.d.ts +52 -0
  156. package/dist/esm/manage/base.d.ts.map +1 -0
  157. package/dist/esm/manage/base.js +63 -0
  158. package/dist/esm/manage/base.js.map +1 -0
  159. package/dist/esm/manage/data.d.ts +100 -0
  160. package/dist/esm/manage/data.d.ts.map +1 -0
  161. package/dist/esm/manage/data.js +130 -0
  162. package/dist/esm/manage/data.js.map +1 -0
  163. package/dist/esm/manage/files.d.ts +45 -0
  164. package/dist/esm/manage/files.d.ts.map +1 -0
  165. package/dist/esm/manage/files.js +68 -0
  166. package/dist/esm/manage/files.js.map +1 -0
  167. package/dist/esm/manage/functions.d.ts +48 -0
  168. package/dist/esm/manage/functions.d.ts.map +1 -0
  169. package/dist/esm/manage/functions.js +57 -0
  170. package/dist/esm/manage/functions.js.map +1 -0
  171. package/dist/esm/manage/index.d.ts +76 -0
  172. package/dist/esm/manage/index.d.ts.map +1 -0
  173. package/dist/esm/manage/index.js +115 -0
  174. package/dist/esm/manage/index.js.map +1 -0
  175. package/dist/esm/manage/lifecycle.d.ts +128 -0
  176. package/dist/esm/manage/lifecycle.d.ts.map +1 -0
  177. package/dist/esm/manage/lifecycle.js +161 -0
  178. package/dist/esm/manage/lifecycle.js.map +1 -0
  179. package/dist/esm/manage/mail.d.ts +16 -0
  180. package/dist/esm/manage/mail.d.ts.map +1 -0
  181. package/dist/esm/manage/mail.js +19 -0
  182. package/dist/esm/manage/mail.js.map +1 -0
  183. package/dist/esm/manage/observe.d.ts +149 -0
  184. package/dist/esm/manage/observe.d.ts.map +1 -0
  185. package/dist/esm/manage/observe.js +161 -0
  186. package/dist/esm/manage/observe.js.map +1 -0
  187. package/dist/esm/manage/realtime.d.ts +52 -0
  188. package/dist/esm/manage/realtime.d.ts.map +1 -0
  189. package/dist/esm/manage/realtime.js +66 -0
  190. package/dist/esm/manage/realtime.js.map +1 -0
  191. package/dist/esm/package.json +1 -0
  192. package/dist/esm/realtime.d.ts +207 -0
  193. package/dist/esm/realtime.d.ts.map +1 -0
  194. package/dist/esm/realtime.js +699 -0
  195. package/dist/esm/realtime.js.map +1 -0
  196. package/dist/esm/session-store.d.ts +16 -0
  197. package/dist/esm/session-store.d.ts.map +1 -0
  198. package/dist/esm/session-store.js +32 -0
  199. package/dist/esm/session-store.js.map +1 -0
  200. package/dist/esm/typegen.d.ts +26 -0
  201. package/dist/esm/typegen.d.ts.map +1 -0
  202. package/dist/esm/typegen.js +163 -0
  203. package/dist/esm/typegen.js.map +1 -0
  204. package/dist/esm/types.d.ts +178 -0
  205. package/dist/esm/types.d.ts.map +1 -0
  206. package/dist/esm/types.js +3 -0
  207. package/dist/esm/types.js.map +1 -0
  208. package/dist/esm/util.d.ts +27 -0
  209. package/dist/esm/util.d.ts.map +1 -0
  210. package/dist/esm/util.js +102 -0
  211. package/dist/esm/util.js.map +1 -0
  212. package/dist/esm/version.d.ts +2 -0
  213. package/dist/esm/version.d.ts.map +1 -0
  214. package/dist/esm/version.js +2 -0
  215. package/dist/esm/version.js.map +1 -0
  216. package/package.json +84 -4
  217. package/src/admin.ts +311 -0
  218. package/src/auth.ts +1180 -0
  219. package/src/cli/gen-types.ts +105 -0
  220. package/src/client.ts +189 -0
  221. package/src/data.ts +576 -0
  222. package/src/errors.ts +330 -0
  223. package/src/files.ts +208 -0
  224. package/src/functions.ts +131 -0
  225. package/src/http.ts +354 -0
  226. package/src/index.ts +56 -0
  227. package/src/manage/access.ts +206 -0
  228. package/src/manage/automation.ts +162 -0
  229. package/src/manage/base.ts +83 -0
  230. package/src/manage/data.ts +159 -0
  231. package/src/manage/files.ts +79 -0
  232. package/src/manage/functions.ts +69 -0
  233. package/src/manage/index.ts +132 -0
  234. package/src/manage/lifecycle.ts +208 -0
  235. package/src/manage/mail.ts +22 -0
  236. package/src/manage/observe.ts +226 -0
  237. package/src/manage/realtime.ts +95 -0
  238. package/src/realtime.ts +765 -0
  239. package/src/session-store.ts +36 -0
  240. package/src/typegen.ts +192 -0
  241. package/src/types.ts +195 -0
  242. package/src/util.ts +114 -0
  243. package/src/version.ts +1 -0
package/src/http.ts ADDED
@@ -0,0 +1,354 @@
1
+ import {
2
+ AbortError,
3
+ NetworkError,
4
+ TimeoutError,
5
+ errorFromResponse,
6
+ parseRetryAfter,
7
+ type ApiError,
8
+ } from "./errors.js";
9
+ import { VERSION } from "./version.js";
10
+ import { anySignal, backoff, queryString, sleep, trimSlash, type QueryValue } from "./util.js";
11
+
12
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
13
+
14
+ export interface RetryOptions {
15
+ /** Extra attempts after the first. `0` disables retries. Default `2`. */
16
+ retries?: number;
17
+ /** First backoff ceiling in milliseconds. Default `300`. */
18
+ baseDelayMs?: number;
19
+ /** Largest backoff in milliseconds. Default `8000`. */
20
+ maxDelayMs?: number;
21
+ /**
22
+ * The longest `Retry-After` the client will wait for. A server asking for more
23
+ * is surfaced as a {@link RateLimitError} instead. Default `30000`.
24
+ */
25
+ maxRetryAfterMs?: number;
26
+ }
27
+
28
+ export interface RequestInfo {
29
+ method: string;
30
+ url: string;
31
+ attempt: number;
32
+ }
33
+
34
+ export interface ResponseInfo extends RequestInfo {
35
+ status: number;
36
+ durationMs: number;
37
+ requestId: string | null;
38
+ }
39
+
40
+ /** Observability hooks. Throwing inside a hook never fails the request. */
41
+ export interface Hooks {
42
+ onRequest?: (info: RequestInfo & { headers: Record<string, string> }) => void;
43
+ onResponse?: (info: ResponseInfo) => void;
44
+ onRetry?: (info: RequestInfo & { reason: string; delayMs: number }) => void;
45
+ onError?: (error: unknown, info: RequestInfo) => void;
46
+ }
47
+
48
+ /** Where the transport gets the signed-in user's access token. */
49
+ export interface TokenProvider {
50
+ /** The current access token, refreshing it first when it is about to expire. `null` when signed out. */
51
+ getAccessToken(): Promise<string | null>;
52
+ /** Called after a `401` for `failed`. Returns a fresh token to retry with, or `null` to give up. */
53
+ onUnauthorized(failed: string): Promise<string | null>;
54
+ }
55
+
56
+ export interface HttpConfig {
57
+ baseUrl: string;
58
+ apiKey: string;
59
+ fetch: FetchLike;
60
+ headers: Record<string, string>;
61
+ timeoutMs: number;
62
+ retry: RetryOptions | false;
63
+ hooks: Hooks;
64
+ tokens: TokenProvider | null;
65
+ }
66
+
67
+ export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream" | "response";
68
+
69
+ export interface RequestOptions {
70
+ method?: string;
71
+ path: string;
72
+ query?: Record<string, QueryValue | QueryValue[]>;
73
+ /** An object is sent as JSON. Strings, `Blob`, `FormData`, buffers and streams are sent as they are. */
74
+ body?: unknown;
75
+ headers?: Record<string, string>;
76
+ signal?: AbortSignal | null | undefined;
77
+ /** Per-attempt timeout. `0` disables it. Default: the client's `timeoutMs`. */
78
+ timeoutMs?: number;
79
+ retry?: RetryOptions | false;
80
+ /**
81
+ * Allow retrying after a network failure or `502`/`503`/`504` even for a method
82
+ * that is not naturally safe to repeat (`POST`, `PATCH`). Only set it when repeating is harmless.
83
+ */
84
+ idempotent?: boolean;
85
+ /** `user`: send the user's token when there is one (default). `none`: the API key alone. */
86
+ auth?: "user" | "none";
87
+ /**
88
+ * Call an absolute URL as it is, sending neither the API key nor a token. For signed
89
+ * URLs, which carry their own authority.
90
+ */
91
+ absoluteUrl?: string;
92
+ /** An explicit user token for this one call, instead of the session's. */
93
+ accessToken?: string | null;
94
+ parse?: ParseAs;
95
+ /** `false`: hand back an error status as a `Response` instead of throwing (after retries). For raw responses, such as HTTP functions. */
96
+ throwOnError?: boolean;
97
+ }
98
+
99
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]);
100
+ const RETRYABLE_STATUS = new Set([502, 503, 504]);
101
+
102
+ const DEFAULT_RETRY: Required<RetryOptions> = {
103
+ retries: 2,
104
+ baseDelayMs: 300,
105
+ maxDelayMs: 8000,
106
+ maxRetryAfterMs: 30_000,
107
+ };
108
+
109
+ function isStream(body: unknown): boolean {
110
+ return typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
111
+ }
112
+
113
+ function isRawBody(body: unknown): boolean {
114
+ return (
115
+ typeof body === "string" ||
116
+ (typeof Blob !== "undefined" && body instanceof Blob) ||
117
+ (typeof FormData !== "undefined" && body instanceof FormData) ||
118
+ (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) ||
119
+ body instanceof ArrayBuffer ||
120
+ ArrayBuffer.isView(body) ||
121
+ isStream(body)
122
+ );
123
+ }
124
+
125
+ export class HttpClient {
126
+ constructor(readonly config: HttpConfig) {}
127
+
128
+ url(path: string, query?: RequestOptions["query"]): string {
129
+ const base = trimSlash(this.config.baseUrl);
130
+ return `${base}${path.startsWith("/") ? path : `/${path}`}${queryString(query)}`;
131
+ }
132
+
133
+ /** Make a request and return the parsed body (`undefined` for `204`). */
134
+ async request<T = unknown>(options: RequestOptions): Promise<T> {
135
+ const response = await this.send(options);
136
+ return (await this.read(response, options.parse ?? "json")) as T;
137
+ }
138
+
139
+ /** Make a request and return the raw `Response`. Non-2xx answers throw. */
140
+ async send(options: RequestOptions): Promise<Response> {
141
+ const method = (options.method ?? "GET").toUpperCase();
142
+ const url = options.absoluteUrl ?? this.url(options.path, options.query);
143
+ const anonymous = options.absoluteUrl !== undefined;
144
+ const retry = this.retryPolicy(options.retry);
145
+ const body = options.body;
146
+ const replayable = !isStream(body);
147
+ const idempotent = options.idempotent === true || SAFE_METHODS.has(method);
148
+
149
+ let token: string | null = null;
150
+ if (anonymous) token = null;
151
+ else if (options.accessToken !== undefined) token = options.accessToken;
152
+ else if (options.auth !== "none" && this.config.tokens) token = await this.config.tokens.getAccessToken();
153
+
154
+ let attempt = 0;
155
+ let refreshed = false;
156
+ for (;;) {
157
+ const info: RequestInfo = { method, url, attempt };
158
+ const started = Date.now();
159
+ let response: Response;
160
+ try {
161
+ response = await this.once(method, url, options, token, info, anonymous);
162
+ } catch (error) {
163
+ const retriable = error instanceof NetworkError || error instanceof TimeoutError;
164
+ if (retriable && retry && idempotent && replayable && attempt < retry.retries) {
165
+ const delayMs = backoff(attempt, retry);
166
+ this.hook(() => this.config.hooks.onRetry?.({ ...info, reason: (error as Error).name, delayMs }));
167
+ await sleep(delayMs, options.signal);
168
+ attempt += 1;
169
+ continue;
170
+ }
171
+ this.hook(() => this.config.hooks.onError?.(error, info));
172
+ throw error;
173
+ }
174
+
175
+ this.hook(() =>
176
+ this.config.hooks.onResponse?.({
177
+ ...info,
178
+ status: response.status,
179
+ durationMs: Date.now() - started,
180
+ requestId: response.headers.get("x-request-id"),
181
+ }),
182
+ );
183
+ if (response.ok) return response;
184
+ if (options.throwOnError === false && !(response.status === 401 && token && !refreshed && this.config.tokens)) {
185
+ const retryable = response.status === 429 || (RETRYABLE_STATUS.has(response.status) && idempotent);
186
+ if (!retry || !retryable || attempt >= retry.retries) return response;
187
+ }
188
+
189
+ // A rejected token: refresh once and replay, unless the body cannot be replayed.
190
+ if (
191
+ response.status === 401 &&
192
+ token &&
193
+ !refreshed &&
194
+ replayable &&
195
+ options.auth !== "none" &&
196
+ options.accessToken === undefined &&
197
+ this.config.tokens
198
+ ) {
199
+ refreshed = true;
200
+ const fresh = await this.config.tokens.onUnauthorized(token);
201
+ if (fresh && fresh !== token) {
202
+ await this.discard(response);
203
+ token = fresh;
204
+ continue;
205
+ }
206
+ }
207
+
208
+ const error = await this.toError(response, method, url);
209
+ const delayMs = this.retryDelay(error, response, attempt, retry, idempotent, replayable);
210
+ if (delayMs !== null && retry) {
211
+ this.hook(() =>
212
+ this.config.hooks.onRetry?.({ ...info, reason: `status ${response.status}`, delayMs }),
213
+ );
214
+ await sleep(delayMs, options.signal);
215
+ attempt += 1;
216
+ continue;
217
+ }
218
+ this.hook(() => this.config.hooks.onError?.(error, info));
219
+ throw error;
220
+ }
221
+ }
222
+
223
+ private retryPolicy(override: RetryOptions | false | undefined): Required<RetryOptions> | null {
224
+ const chosen = override === undefined ? this.config.retry : override;
225
+ if (chosen === false) return null;
226
+ const merged = { ...DEFAULT_RETRY, ...(this.config.retry === false ? {} : this.config.retry), ...chosen };
227
+ return merged.retries > 0 ? merged : null;
228
+ }
229
+
230
+ private retryDelay(
231
+ error: ApiError,
232
+ response: Response,
233
+ attempt: number,
234
+ retry: Required<RetryOptions> | null,
235
+ idempotent: boolean,
236
+ replayable: boolean,
237
+ ): number | null {
238
+ if (!retry || !replayable || attempt >= retry.retries) return null;
239
+ const status = response.status;
240
+ // 429 is answered before a handler runs, so repeating any method is safe.
241
+ const retryable = status === 429 || (RETRYABLE_STATUS.has(status) && idempotent);
242
+ if (!retryable) return null;
243
+ const asked = parseRetryAfter(response.headers.get("retry-after")) ?? error.retryAfter;
244
+ if (asked !== null && asked !== undefined) {
245
+ const ms = asked * 1000;
246
+ return ms > retry.maxRetryAfterMs ? null : Math.ceil(ms);
247
+ }
248
+ return backoff(attempt, retry);
249
+ }
250
+
251
+ private async once(
252
+ method: string,
253
+ url: string,
254
+ options: RequestOptions,
255
+ token: string | null,
256
+ info: RequestInfo,
257
+ anonymous: boolean,
258
+ ): Promise<Response> {
259
+ const headers: Record<string, string> = {
260
+ accept: "application/json",
261
+ ...(anonymous ? {} : { apikey: this.config.apiKey, "x-client-info": `pawabase-js/${VERSION}`, ...this.config.headers }),
262
+ ...options.headers,
263
+ };
264
+ if (token) headers["authorization"] = `Bearer ${token}`;
265
+
266
+ let body: BodyInit | undefined;
267
+ const raw = options.body;
268
+ if (raw !== undefined && raw !== null) {
269
+ if (isRawBody(raw)) {
270
+ body = raw as BodyInit;
271
+ } else {
272
+ body = JSON.stringify(raw);
273
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
274
+ headers["content-type"] = "application/json";
275
+ }
276
+ }
277
+ }
278
+
279
+ this.hook(() =>
280
+ this.config.hooks.onRequest?.({
281
+ ...info,
282
+ headers: { ...headers, ...(headers["apikey"] ? { apikey: "***" } : {}), ...(headers["authorization"] ? { authorization: "Bearer ***" } : {}) },
283
+ }),
284
+ );
285
+
286
+ const timeoutMs = options.timeoutMs ?? this.config.timeoutMs;
287
+ const timeoutController = new AbortController();
288
+ const timer = timeoutMs > 0 ? setTimeout(() => timeoutController.abort(new TimeoutError(timeoutMs)), timeoutMs) : null;
289
+ const signal = anySignal([options.signal, timeoutMs > 0 ? timeoutController.signal : undefined]);
290
+ const init: RequestInit & { duplex?: "half" } = { method, headers, ...(signal ? { signal } : {}) };
291
+ if (body !== undefined) {
292
+ init.body = body;
293
+ if (isStream(body)) init.duplex = "half";
294
+ }
295
+ try {
296
+ return await this.config.fetch(url, init);
297
+ } catch (error) {
298
+ if (options.signal?.aborted) throw new AbortError(undefined, { cause: error });
299
+ if (timeoutController.signal.aborted) throw new TimeoutError(timeoutMs, { cause: error });
300
+ throw new NetworkError(`Could not reach ${new URL(url).origin}: ${(error as Error)?.message ?? error}`, {
301
+ cause: error,
302
+ method,
303
+ url,
304
+ });
305
+ } finally {
306
+ if (timer) clearTimeout(timer);
307
+ }
308
+ }
309
+
310
+ private async toError(response: Response, method: string, url: string): Promise<ApiError> {
311
+ let body: unknown;
312
+ const text = await response.text().catch(() => "");
313
+ if (text) {
314
+ try {
315
+ body = JSON.parse(text);
316
+ } catch {
317
+ body = text;
318
+ }
319
+ }
320
+ return errorFromResponse({ status: response.status, body, headers: response.headers, method, url });
321
+ }
322
+
323
+ private async discard(response: Response): Promise<void> {
324
+ try {
325
+ await response.arrayBuffer();
326
+ } catch {
327
+ /* the body is not needed */
328
+ }
329
+ }
330
+
331
+ async read(response: Response, as: ParseAs): Promise<unknown> {
332
+ if (as === "response") return response;
333
+ if (as === "stream") return response.body;
334
+ if (as === "blob") return response.blob();
335
+ if (as === "arrayBuffer") return response.arrayBuffer();
336
+ if (response.status === 204 || response.status === 205) return undefined;
337
+ const text = await response.text();
338
+ if (!text) return undefined;
339
+ if (as === "text") return text;
340
+ try {
341
+ return JSON.parse(text);
342
+ } catch {
343
+ return text;
344
+ }
345
+ }
346
+
347
+ private hook(fn: () => void): void {
348
+ try {
349
+ fn();
350
+ } catch {
351
+ /* hooks observe; they must not break requests */
352
+ }
353
+ }
354
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ export { PawabaseClient, createClient } from "./client.js";
2
+ export type { ClientOptions, DatabaseShape } from "./client.js";
3
+
4
+ export { AuthClient } from "./auth.js";
5
+ export type {
6
+ AuthChangeEvent,
7
+ AuthListener,
8
+ AuthOptions,
9
+ IssuedToken,
10
+ Passkey,
11
+ PasskeyChallenge,
12
+ SignInResult,
13
+ SignUpResult,
14
+ UpdateUserInput,
15
+ UrlSessionResult,
16
+ } from "./auth.js";
17
+ export { MemoryStorage } from "./session-store.js";
18
+ export type { SessionStorage } from "./session-store.js";
19
+
20
+ export { QueryBuilder, ResourceClient } from "./data.js";
21
+ export type {
22
+ AnyRow,
23
+ Condition,
24
+ CreateManyResult,
25
+ FilterOp,
26
+ IterateOptions,
27
+ QueryOptions,
28
+ ReadOptions,
29
+ ResourceTypes,
30
+ } from "./data.js";
31
+
32
+ export { FlowsClient, FunctionsClient, RoutesClient } from "./functions.js";
33
+ export type { CallOptions, RouteOptions } from "./functions.js";
34
+
35
+ export { BucketClient, StorageClient } from "./files.js";
36
+ export type { TransferOptions, UploadBody, UploadOptions } from "./files.js";
37
+
38
+ export { RealtimeChannel, RealtimeClient } from "./realtime.js";
39
+ export type {
40
+ ChannelEvents,
41
+ ChannelOptions,
42
+ PresenceChange,
43
+ RealtimeOptions,
44
+ RealtimeStatus,
45
+ WebSocketConstructor,
46
+ WebSocketLike,
47
+ } from "./realtime.js";
48
+
49
+ export { HttpClient } from "./http.js";
50
+ export type { FetchLike, Hooks, RequestInfo, RequestOptions, ResponseInfo, RetryOptions, TokenProvider } from "./http.js";
51
+
52
+ export * from "./errors.js";
53
+ export type * from "./types.js";
54
+ export { VERSION } from "./version.js";
55
+ export { generateTypes } from "./typegen.js";
56
+ export type { TypegenOptions } from "./typegen.js";
@@ -0,0 +1,206 @@
1
+ import { Service, seg, type Api, type Body } from "./base.js";
2
+
3
+ type When = Date | string;
4
+ const iso = (value?: When): string | undefined => (value instanceof Date ? value.toISOString() : value);
5
+
6
+ /** The environment's API keys: publishable (`pb_pk_…`, safe in apps) and secret (`pb_sk_…`, servers only). */
7
+ export class Keys extends Service {
8
+ list(): Promise<Api> {
9
+ return this.get("/keys");
10
+ }
11
+
12
+ /** Every scope a key can hold, with what each allows (the choices for `create({ scopes })`). */
13
+ scopes(): Promise<Api> {
14
+ return this.project("GET", "/key-scopes");
15
+ }
16
+
17
+ /**
18
+ * Make a key; the key itself is in the answer once. `scopes` limit what it may do (`resource:read`, `functions:invoke`…),
19
+ * `allowedIps` and `allowedRoutes` (`"GET /rest/v1/orders"`, `"/functions/v1/*"`) limit where and what it may call.
20
+ */
21
+ create(name: string, options: { role?: "publishable" | "secret"; scopes?: string[]; expiresAt?: When; allowedIps?: string[]; allowedRoutes?: string[] } = {}): Promise<Api> {
22
+ return this.post("/keys", {
23
+ name,
24
+ role: options.role ?? "publishable",
25
+ scopes: options.scopes ?? [],
26
+ expires_at: iso(options.expiresAt),
27
+ allowed_ips: options.allowedIps ?? [],
28
+ allowed_routes: options.allowedRoutes ?? [],
29
+ });
30
+ }
31
+
32
+ revoke(keyId: string): Promise<Api> {
33
+ return this.post(seg("keys", keyId, "revoke"));
34
+ }
35
+
36
+ /** Replace a key with a new one; the old keeps working for `graceHours` (24 by default) so clients can switch. */
37
+ rotate(keyId: string, options: { graceHours?: number } = {}): Promise<Api> {
38
+ return this.post(seg("keys", keyId, "rotate"), { grace_hours: options.graceHours ?? 24 });
39
+ }
40
+ }
41
+
42
+ /** Secrets functions and flows read (`ctx.runtime.secret(name)`). Values are never returned. */
43
+ export class Secrets extends Service {
44
+ list(): Promise<Api> {
45
+ return this.get("/secrets");
46
+ }
47
+
48
+ set(name: string, value: string, options: { description?: string; rotateEveryDays?: number } = {}): Promise<Api> {
49
+ return this.put(seg("secrets", name), { value, description: options.description ?? "", rotate_every_days: options.rotateEveryDays });
50
+ }
51
+
52
+ /** Change the description or the rotation reminder (`0` clears it). */
53
+ update(name: string, options: { description?: string; rotateEveryDays?: number }): Promise<Api> {
54
+ return this.patch(seg("secrets", name), { description: options.description, rotate_every_days: options.rotateEveryDays });
55
+ }
56
+
57
+ /** Replace the value, keeping the old one for {@link rollback}. */
58
+ rotate(name: string, value?: string): Promise<Api> {
59
+ return this.post(seg("secrets", name, "rotate"), value ? { value } : {});
60
+ }
61
+
62
+ /** Put back the value a rotation replaced. */
63
+ rollback(name: string): Promise<Api> {
64
+ return this.post(seg("secrets", name, "rollback"));
65
+ }
66
+
67
+ remove(name: string): Promise<Api> {
68
+ return this.del(seg("secrets", name));
69
+ }
70
+ }
71
+
72
+ /** Custom domains the environment answers on, proved with a DNS record. */
73
+ export class Domains extends Service {
74
+ list(): Promise<Api> {
75
+ return this.get("/domains");
76
+ }
77
+
78
+ /** Register a hostname: the answer says which DNS record proves you own it. */
79
+ add(hostname: string): Promise<Api> {
80
+ return this.post("/domains", { hostname });
81
+ }
82
+
83
+ /** Check the DNS record now. */
84
+ verify(domainId: string): Promise<Api> {
85
+ return this.post(seg("domains", domainId, "verify"));
86
+ }
87
+
88
+ remove(domainId: string): Promise<Api> {
89
+ return this.del(seg("domains", domainId));
90
+ }
91
+ }
92
+
93
+ export interface FirewallCondition {
94
+ /** `ip`, `country`, `method`, `path`, `host`, `query`, `header:<name>`, `query:<name>`, `user_agent`, `origin`, `referer`, `role`, `key_id`, `content_length`. */
95
+ field: string;
96
+ op: string;
97
+ value: unknown;
98
+ }
99
+
100
+ export interface FirewallRule {
101
+ name: string;
102
+ action?: "allow" | "block" | "log" | "rate_limit";
103
+ conditions?: FirewallCondition[];
104
+ /** How `conditions` combine: `all` (default) or `any`. */
105
+ logic?: "all" | "any";
106
+ match?: Record<string, string[]>;
107
+ rateLimit?: { requests: number; window_seconds: number; by?: string; ban_seconds?: number };
108
+ /** `enforce` (default) or `simulate`, which only records what the rule would have done. */
109
+ mode?: "enforce" | "simulate";
110
+ enabled?: boolean;
111
+ expiresAt?: When;
112
+ note?: string;
113
+ }
114
+
115
+ /**
116
+ * The firewall at the gateway: ordered rules (allow, block, log, rate limit), managed attack signatures, bans, and what it did.
117
+ *
118
+ * ```ts
119
+ * await m.firewall.addRule({ name: "Burst", action: "rate_limit",
120
+ * conditions: [{ field: "path", op: "starts_with", value: "/auth/v1/token" }],
121
+ * rateLimit: { requests: 10, window_seconds: 60, by: "ip", ban_seconds: 900 } });
122
+ * ```
123
+ */
124
+ export class Firewall extends Service {
125
+ /** Rules in the order they are tried. */
126
+ rules(): Promise<Api> {
127
+ return this.get("/firewall");
128
+ }
129
+
130
+ /** Add a rule at the end. */
131
+ addRule(rule: FirewallRule): Promise<Api> {
132
+ return this.post("/firewall", {
133
+ name: rule.name,
134
+ action: rule.action ?? "block",
135
+ conditions: rule.conditions ?? [],
136
+ logic: rule.logic ?? "all",
137
+ match: rule.match ?? {},
138
+ rate_limit: rule.rateLimit,
139
+ mode: rule.mode ?? "enforce",
140
+ enabled: rule.enabled ?? true,
141
+ expires_at: iso(rule.expiresAt),
142
+ note: rule.note ?? "",
143
+ });
144
+ }
145
+
146
+ /** Change some fields of a rule, using the server's field names (`clear_expiry: true` removes an expiry). */
147
+ updateRule(ruleId: string, changes: Body): Promise<Api> {
148
+ return this.patch(seg("firewall", ruleId), changes);
149
+ }
150
+
151
+ removeRule(ruleId: string): Promise<Api> {
152
+ return this.del(seg("firewall", ruleId));
153
+ }
154
+
155
+ /** Set the order rules are tried in: every rule exactly once. */
156
+ order(ruleIds: string[]): Promise<Api> {
157
+ return this.put("/firewall-order", { ids: ruleIds });
158
+ }
159
+
160
+ /** Managed rule sets (`sqli`, `xss`, `traversal`, `scanners`, `sensitive_files`, `no_user_agent`, `methods`) and automatic bans. */
161
+ config(): Promise<Api> {
162
+ return this.get("/firewall-config");
163
+ }
164
+
165
+ /** `managed`: `{ sqli: "block", scanners: "log" }` (`off`, `log`, `block`). `autoBan`: `{ enabled, threshold, window_seconds, ban_seconds, statuses, exempt }`. */
166
+ setConfig(options: { managed?: Record<string, string>; autoBan?: Body }): Promise<Api> {
167
+ return this.put("/firewall-config", { managed: options.managed, auto_ban: options.autoBan });
168
+ }
169
+
170
+ bans(options: { includeExpired?: boolean } = {}): Promise<Api> {
171
+ return this.get("/firewall-bans", { all: options.includeExpired ? 1 : undefined });
172
+ }
173
+
174
+ /** Ban an address or CIDR range for `seconds` (`0`: until lifted). */
175
+ ban(ip: string, options: { seconds?: number; reason?: string } = {}): Promise<Api> {
176
+ return this.post("/firewall-bans", { ip, seconds: options.seconds ?? 0, reason: options.reason ?? "" });
177
+ }
178
+
179
+ liftBan(banId: string): Promise<Api> {
180
+ return this.del(seg("firewall-bans", banId));
181
+ }
182
+
183
+ events(options: { action?: string; source?: string; ip?: string; path?: string; ruleId?: string; limit?: number } = {}): Promise<Api> {
184
+ const { ruleId, ...rest } = options;
185
+ return this.get("/firewall-events", { limit: 100, ...rest, rule_id: ruleId });
186
+ }
187
+
188
+ /** Totals, a timeline, and the top addresses, paths, rules and countries. */
189
+ stats(options: { hours?: number } = {}): Promise<Api> {
190
+ return this.get("/firewall-stats", { hours: options.hours ?? 24 });
191
+ }
192
+
193
+ /** What the firewall would do with a request, step by step. Nothing is sent or counted. */
194
+ test(ip: string, options: { path?: string; method?: string; userAgent?: string; query?: string; headers?: Record<string, string>; country?: string; role?: string } = {}): Promise<Api> {
195
+ return this.post("/firewall-test", {
196
+ ip,
197
+ path: options.path ?? "/",
198
+ method: options.method ?? "GET",
199
+ user_agent: options.userAgent ?? "",
200
+ query: options.query ?? "",
201
+ headers: options.headers ?? {},
202
+ country: options.country ?? "",
203
+ role: options.role ?? "none",
204
+ });
205
+ }
206
+ }