@fonderie/client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/signatures.md +39 -0
- package/dist/index.cjs +181 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -2
- package/dist/index.d.ts +65 -2
- package/dist/index.js +179 -18
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/brain/signatures.md
CHANGED
|
@@ -5,11 +5,41 @@
|
|
|
5
5
|
## @fonderie/client
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
+
interface IClientAuthConfig {
|
|
9
|
+
getRefreshToken?: () => string | undefined;
|
|
10
|
+
onTokensChanged?: (tokens: ITokens) => void;
|
|
11
|
+
onAuthError?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
interface IFonderieClientOptions {
|
|
9
15
|
baseUrl: string;
|
|
10
16
|
accessToken?: string;
|
|
17
|
+
workspaceId?: string;
|
|
18
|
+
cache?: ICache;
|
|
19
|
+
auth?: IClientAuthConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IRequestConfig {
|
|
23
|
+
workspaceId?: string;
|
|
24
|
+
cache?: number | false;
|
|
25
|
+
bust?: boolean;
|
|
26
|
+
invalidate?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ICache {
|
|
30
|
+
get<T>(key: string): T | undefined;
|
|
31
|
+
set<T>(key: string, value: T, ttlMs: number): void;
|
|
32
|
+
dedupe<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
33
|
+
invalidate(fragment: string): void;
|
|
34
|
+
clear(): void;
|
|
11
35
|
}
|
|
12
36
|
|
|
37
|
+
interface IMemoryCacheOptions {
|
|
38
|
+
defaultTtlMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createMemoryCache(opts?: IMemoryCacheOptions): ICache & { defaultTtlMs: number; }
|
|
42
|
+
|
|
13
43
|
new FonderieClient(opts: IFonderieClientOptions): FonderieClient
|
|
14
44
|
.auth: AuthClient
|
|
15
45
|
.billing: BillingClient
|
|
@@ -17,6 +47,15 @@ new FonderieClient(opts: IFonderieClientOptions): FonderieClient
|
|
|
17
47
|
.audit: AuditClient
|
|
18
48
|
.webhooks: WebhooksClient
|
|
19
49
|
.customers: CustomersClient
|
|
50
|
+
.setAccessToken(token: string | undefined): void
|
|
51
|
+
.clearCache(): void
|
|
52
|
+
.setWorkspaceId(workspaceId: string | undefined): void
|
|
53
|
+
.request<T = unknown>(opts: { method: string; path: string; body?: unknown; workspaceId?: string | undefined; cache?: number | false | undefined; bust?: boolean | undefined; invalidate?: string[] | undefined; }): Promise<IApiResponse<...>>
|
|
54
|
+
.get<T = unknown>(path: string, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
|
|
55
|
+
.post<T = unknown>(path: string, body?: unknown, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
|
|
56
|
+
.put<T = unknown>(path: string, body?: unknown, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
|
|
57
|
+
.patch<T = unknown>(path: string, body?: unknown, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
|
|
58
|
+
.delete<T = unknown>(path: string, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
|
|
20
59
|
|
|
21
60
|
new FonderieApiError(reason: string, explanation: string, status: number, details?: unknown): FonderieApiError
|
|
22
61
|
.reason: string
|
package/dist/index.cjs
CHANGED
|
@@ -29,10 +29,48 @@ __export(index_exports, {
|
|
|
29
29
|
FonderieApiError: () => FonderieApiError,
|
|
30
30
|
FonderieClient: () => FonderieClient,
|
|
31
31
|
WebhooksClient: () => WebhooksClient,
|
|
32
|
-
WorkspacesClient: () => WorkspacesClient
|
|
32
|
+
WorkspacesClient: () => WorkspacesClient,
|
|
33
|
+
createMemoryCache: () => createMemoryCache
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(index_exports);
|
|
35
36
|
|
|
37
|
+
// src/cache.ts
|
|
38
|
+
function createMemoryCache(opts = {}) {
|
|
39
|
+
const store = /* @__PURE__ */ new Map();
|
|
40
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
41
|
+
const defaultTtlMs = opts.defaultTtlMs ?? 6e4;
|
|
42
|
+
return {
|
|
43
|
+
defaultTtlMs,
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = store.get(key);
|
|
46
|
+
if (!entry) return void 0;
|
|
47
|
+
if (Date.now() > entry.expiresAt) {
|
|
48
|
+
store.delete(key);
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return entry.value;
|
|
52
|
+
},
|
|
53
|
+
set(key, value, ttlMs) {
|
|
54
|
+
store.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
55
|
+
},
|
|
56
|
+
dedupe(key, fn) {
|
|
57
|
+
const existing = inflight.get(key);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const req = fn().finally(() => inflight.delete(key));
|
|
60
|
+
inflight.set(key, req);
|
|
61
|
+
return req;
|
|
62
|
+
},
|
|
63
|
+
invalidate(fragment) {
|
|
64
|
+
for (const key of store.keys()) if (key.includes(fragment)) store.delete(key);
|
|
65
|
+
for (const key of inflight.keys()) if (key.includes(fragment)) inflight.delete(key);
|
|
66
|
+
},
|
|
67
|
+
clear() {
|
|
68
|
+
store.clear();
|
|
69
|
+
inflight.clear();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
// src/http.ts
|
|
37
75
|
var FonderieApiError = class extends Error {
|
|
38
76
|
constructor(reason, explanation, status, details) {
|
|
@@ -49,24 +87,52 @@ var FonderieApiError = class extends Error {
|
|
|
49
87
|
details;
|
|
50
88
|
};
|
|
51
89
|
var HttpClient = class {
|
|
52
|
-
constructor(baseUrl) {
|
|
90
|
+
constructor(baseUrl, deps = {}) {
|
|
53
91
|
this.baseUrl = baseUrl;
|
|
92
|
+
this.cache = deps.cache;
|
|
93
|
+
this.defaultTtlMs = deps.defaultTtlMs ?? 6e4;
|
|
94
|
+
this.refresh = deps.refresh;
|
|
54
95
|
}
|
|
55
96
|
baseUrl;
|
|
97
|
+
cache;
|
|
98
|
+
defaultTtlMs;
|
|
99
|
+
refresh;
|
|
56
100
|
async request(opts) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
const method = opts.method.toUpperCase();
|
|
102
|
+
const cache = this.cache;
|
|
103
|
+
if (cache && method === "GET" && opts.cache !== false) {
|
|
104
|
+
const key = `GET ${opts.path}::ws=${opts.workspaceId ?? ""}`;
|
|
105
|
+
if (!opts.bust) {
|
|
106
|
+
const hit = cache.get(key);
|
|
107
|
+
if (hit !== void 0) return hit;
|
|
108
|
+
}
|
|
109
|
+
const ttl = typeof opts.cache === "number" ? opts.cache : this.defaultTtlMs;
|
|
110
|
+
return cache.dedupe(key, async () => {
|
|
111
|
+
const data2 = await this.exec(opts);
|
|
112
|
+
cache.set(key, data2, ttl);
|
|
113
|
+
return data2;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const data = await this.exec(opts);
|
|
117
|
+
if (cache && method !== "GET") {
|
|
118
|
+
const resource = opts.path.split("?")[0]?.split("/").filter(Boolean)[0];
|
|
119
|
+
if (resource) cache.invalidate(`/${resource}`);
|
|
120
|
+
for (const fragment of opts.invalidate ?? []) cache.invalidate(fragment);
|
|
121
|
+
}
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
async exec(opts, retried = false) {
|
|
125
|
+
const headers = { "Content-Type": "application/json" };
|
|
60
126
|
if (opts.token) headers["Authorization"] = `Bearer ${opts.token}`;
|
|
61
127
|
if (opts.cookie) headers["Cookie"] = opts.cookie;
|
|
62
128
|
if (opts.workspaceId) headers["X-Workspace-ID"] = opts.workspaceId;
|
|
63
|
-
const fetchInit = {
|
|
64
|
-
method: opts.method,
|
|
65
|
-
headers,
|
|
66
|
-
credentials: "include"
|
|
67
|
-
};
|
|
129
|
+
const fetchInit = { method: opts.method, headers, credentials: "include" };
|
|
68
130
|
if (opts.body !== void 0) fetchInit.body = JSON.stringify(opts.body);
|
|
69
131
|
const res = await fetch(`${this.baseUrl}${opts.path}`, fetchInit);
|
|
132
|
+
if (res.status === 401 && this.refresh && !retried && !opts.path.startsWith("/auth/")) {
|
|
133
|
+
const newToken = await this.refresh();
|
|
134
|
+
if (newToken) return this.exec({ ...opts, token: newToken }, true);
|
|
135
|
+
}
|
|
70
136
|
if (res.status === 204) {
|
|
71
137
|
if (!res.ok) throw new FonderieApiError("unknown", res.statusText, res.status);
|
|
72
138
|
return void 0;
|
|
@@ -1062,15 +1128,110 @@ var FonderieClient = class {
|
|
|
1062
1128
|
webhooks;
|
|
1063
1129
|
customers;
|
|
1064
1130
|
http;
|
|
1131
|
+
tokens;
|
|
1132
|
+
workspaceId;
|
|
1133
|
+
cache;
|
|
1134
|
+
authConfig;
|
|
1135
|
+
refreshing = null;
|
|
1065
1136
|
constructor(opts) {
|
|
1066
|
-
this.
|
|
1067
|
-
|
|
1068
|
-
this.
|
|
1069
|
-
this.
|
|
1070
|
-
this.
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1137
|
+
this.tokens = new TokenStore(opts.accessToken);
|
|
1138
|
+
this.workspaceId = opts.workspaceId;
|
|
1139
|
+
this.cache = opts.cache;
|
|
1140
|
+
this.authConfig = opts.auth;
|
|
1141
|
+
this.http = new HttpClient(opts.baseUrl, {
|
|
1142
|
+
cache: opts.cache,
|
|
1143
|
+
defaultTtlMs: opts.cache?.defaultTtlMs,
|
|
1144
|
+
refresh: opts.auth ? () => this.doRefresh() : void 0
|
|
1145
|
+
});
|
|
1146
|
+
this.auth = new AuthClient(this.http, this.tokens);
|
|
1147
|
+
this.billing = new BillingClient(this.http, this.tokens);
|
|
1148
|
+
this.workspaces = new WorkspacesClient(this.http, this.tokens);
|
|
1149
|
+
this.audit = new AuditClient(this.http, this.tokens);
|
|
1150
|
+
this.webhooks = new WebhooksClient(this.http, this.tokens);
|
|
1151
|
+
this.customers = new CustomersClient(this.http, this.tokens);
|
|
1152
|
+
}
|
|
1153
|
+
// Single-flight refresh: POST /auth/refresh with the app-supplied refresh
|
|
1154
|
+
// token, store the new access token, notify the app, return it for the retry.
|
|
1155
|
+
doRefresh() {
|
|
1156
|
+
if (this.refreshing) return this.refreshing;
|
|
1157
|
+
this.refreshing = (async () => {
|
|
1158
|
+
const refreshToken = this.authConfig?.getRefreshToken?.();
|
|
1159
|
+
if (!refreshToken) {
|
|
1160
|
+
this.authConfig?.onAuthError?.();
|
|
1161
|
+
return void 0;
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
const { result } = await this.auth.refreshTokens(refreshToken);
|
|
1165
|
+
const tokens = result.tokens;
|
|
1166
|
+
this.tokens.set(tokens.access);
|
|
1167
|
+
this.authConfig?.onTokensChanged?.(tokens);
|
|
1168
|
+
return tokens.access;
|
|
1169
|
+
} catch {
|
|
1170
|
+
this.tokens.set(void 0);
|
|
1171
|
+
this.authConfig?.onAuthError?.();
|
|
1172
|
+
return void 0;
|
|
1173
|
+
} finally {
|
|
1174
|
+
this.refreshing = null;
|
|
1175
|
+
}
|
|
1176
|
+
})();
|
|
1177
|
+
return this.refreshing;
|
|
1178
|
+
}
|
|
1179
|
+
// The JWT used to authenticate every request — the typed modules and the
|
|
1180
|
+
// generic transport share one token store. Passing undefined signs out and
|
|
1181
|
+
// clears the cache (so no session's data survives a logout).
|
|
1182
|
+
setAccessToken(token) {
|
|
1183
|
+
this.tokens.set(token);
|
|
1184
|
+
if (!token) this.clearCache();
|
|
1185
|
+
}
|
|
1186
|
+
// Drop all cached responses (e.g. on switching accounts).
|
|
1187
|
+
clearCache() {
|
|
1188
|
+
this.cache?.clear();
|
|
1189
|
+
}
|
|
1190
|
+
// Default X-Workspace-ID for the generic transport, also propagated to the
|
|
1191
|
+
// workspace-scoped modules so one call configures the whole client.
|
|
1192
|
+
setWorkspaceId(workspaceId) {
|
|
1193
|
+
this.workspaceId = workspaceId;
|
|
1194
|
+
this.billing.setWorkspaceId(workspaceId);
|
|
1195
|
+
this.workspaces.setWorkspaceId(workspaceId);
|
|
1196
|
+
this.customers.setWorkspaceId(workspaceId);
|
|
1197
|
+
}
|
|
1198
|
+
// ── Generic transport ──────────────────────────────────────────────────────
|
|
1199
|
+
// A Fonderie-aware HTTP client for endpoints outside the typed modules (an
|
|
1200
|
+
// app's own routes on the same backend). It attaches the shared JWT and
|
|
1201
|
+
// X-Workspace-ID automatically and returns Fonderie's { reason, explanation,
|
|
1202
|
+
// result } envelope — so you don't hand-roll auth for custom endpoints.
|
|
1203
|
+
request(opts) {
|
|
1204
|
+
return this.http.request({
|
|
1205
|
+
method: opts.method,
|
|
1206
|
+
path: opts.path,
|
|
1207
|
+
body: opts.body,
|
|
1208
|
+
token: this.tokens.get(),
|
|
1209
|
+
workspaceId: opts.workspaceId ?? this.workspaceId,
|
|
1210
|
+
cache: opts.cache,
|
|
1211
|
+
bust: opts.bust,
|
|
1212
|
+
invalidate: opts.invalidate
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
get(path, config) {
|
|
1216
|
+
return this.request({
|
|
1217
|
+
method: "GET",
|
|
1218
|
+
path,
|
|
1219
|
+
workspaceId: config?.workspaceId,
|
|
1220
|
+
cache: config?.cache,
|
|
1221
|
+
bust: config?.bust
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
post(path, body, config) {
|
|
1225
|
+
return this.request({ method: "POST", path, body, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
|
|
1226
|
+
}
|
|
1227
|
+
put(path, body, config) {
|
|
1228
|
+
return this.request({ method: "PUT", path, body, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
|
|
1229
|
+
}
|
|
1230
|
+
patch(path, body, config) {
|
|
1231
|
+
return this.request({ method: "PATCH", path, body, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
|
|
1232
|
+
}
|
|
1233
|
+
delete(path, config) {
|
|
1234
|
+
return this.request({ method: "DELETE", path, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
|
|
1074
1235
|
}
|
|
1075
1236
|
};
|
|
1076
1237
|
|
|
@@ -1254,6 +1415,7 @@ var CourierAdminClient = class {
|
|
|
1254
1415
|
FonderieApiError,
|
|
1255
1416
|
FonderieClient,
|
|
1256
1417
|
WebhooksClient,
|
|
1257
|
-
WorkspacesClient
|
|
1418
|
+
WorkspacesClient,
|
|
1419
|
+
createMemoryCache
|
|
1258
1420
|
});
|
|
1259
1421
|
//# sourceMappingURL=index.cjs.map
|