@heyramzi/utils 0.0.0-stage → 1.12.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/package.json +103 -4
  4. package/src/ai/image-agnes.test.ts +130 -0
  5. package/src/ai/image.ts +435 -0
  6. package/src/ai/models.test.ts +98 -0
  7. package/src/ai/models.ts +450 -0
  8. package/src/analytics/client.test.ts +149 -0
  9. package/src/analytics/client.ts +129 -0
  10. package/src/analytics/server.test.ts +165 -0
  11. package/src/analytics/server.ts +88 -0
  12. package/src/analytics/svelte.test.ts +105 -0
  13. package/src/analytics/svelte.ts +58 -0
  14. package/src/analytics/types.ts +26 -0
  15. package/src/analytics/umami/protocol.test.ts +18 -0
  16. package/src/analytics/umami/protocol.ts +104 -0
  17. package/src/analytics/umami/reporting.test.ts +391 -0
  18. package/src/analytics/umami/reporting.ts +627 -0
  19. package/src/auth/config/otp.ts +8 -0
  20. package/src/auth/config/rate-limits.ts +11 -0
  21. package/src/auth/config/trusted-origins.ts +32 -0
  22. package/src/auth/email/index.ts +7 -0
  23. package/src/auth/email/layout.test.ts +73 -0
  24. package/src/auth/email/layout.ts +122 -0
  25. package/src/auth/email/resend.test.ts +35 -0
  26. package/src/auth/email/resend.ts +26 -0
  27. package/src/auth/email/template.ts +77 -0
  28. package/src/auth/email/types.ts +74 -0
  29. package/src/auth/index.ts +5 -0
  30. package/src/cache.test.ts +67 -0
  31. package/src/cache.ts +5 -0
  32. package/src/cn.test.ts +52 -0
  33. package/src/cn.ts +16 -0
  34. package/src/collection.test.ts +66 -0
  35. package/src/collection.ts +8 -0
  36. package/src/crypto.test.ts +86 -0
  37. package/src/crypto.ts +157 -0
  38. package/src/error.test.ts +79 -0
  39. package/src/error.ts +16 -0
  40. package/src/errors/sentry/index.ts +20 -0
  41. package/src/errors/sentry/protocol.test.ts +85 -0
  42. package/src/errors/sentry/protocol.ts +144 -0
  43. package/src/errors/sentry/sveltekit.test.ts +88 -0
  44. package/src/errors/sentry/sveltekit.ts +188 -0
  45. package/src/format.test.ts +225 -0
  46. package/src/format.ts +103 -0
  47. package/src/grab.d.ts +2 -0
  48. package/src/grab.js +112 -0
  49. package/src/index.ts +38 -0
  50. package/src/logger-types.ts +14 -0
  51. package/src/logger.ts +24 -0
  52. package/src/math.test.ts +41 -0
  53. package/src/math.ts +3 -0
  54. package/src/og/fonts.ts +69 -0
  55. package/src/og/index.ts +5 -0
  56. package/src/og/markup.test.ts +73 -0
  57. package/src/og/markup.ts +90 -0
  58. package/src/og/renderer.ts +42 -0
  59. package/src/og/types.ts +26 -0
  60. package/src/og/wawoff2.d.ts +6 -0
  61. package/src/package-imports.test.mjs +22 -0
  62. package/src/result.test.ts +149 -0
  63. package/src/result.ts +29 -0
  64. package/src/string.test.ts +205 -0
  65. package/src/string.ts +71 -0
  66. package/src/testimonials/render-content.ts +21 -0
  67. package/src/testimonials/server.ts +47 -0
  68. package/src/testimonials/types.ts +35 -0
  69. package/src/timing.test.ts +166 -0
  70. package/src/timing.ts +28 -0
  71. package/src/vite.d.ts +12 -0
  72. package/src/vite.js +265 -0
  73. package/src/vite.test.js +235 -0
@@ -0,0 +1,391 @@
1
+ import { describe, expect, it, vi } from "vite-plus/test";
2
+ import { UmamiReportingClient, type UmamiFetch } from "#src/analytics/umami/reporting.js";
3
+
4
+ type Route = { status?: number; body: unknown };
5
+
6
+ /**
7
+ * A fetch stand-in that answers by path suffix and records every call, so a
8
+ * test can assert on headers and on how many times the network was touched.
9
+ */
10
+ function makeFetch(routes: Record<string, Route>) {
11
+ const calls: {
12
+ url: string;
13
+ method?: string;
14
+ headers: Record<string, string>;
15
+ body?: string;
16
+ }[] = [];
17
+
18
+ const impl: UmamiFetch = async (url, init) => {
19
+ calls.push({ url, method: init?.method, headers: init?.headers ?? {}, body: init?.body });
20
+ const key = Object.keys(routes).find((path) => url.includes(path));
21
+ const route = key ? routes[key] : undefined;
22
+ if (!route) {
23
+ return { ok: false, status: 404, statusText: "Not Found", json: async () => ({}) };
24
+ }
25
+ const status = route.status ?? 200;
26
+ return {
27
+ ok: status >= 200 && status < 300,
28
+ status,
29
+ statusText: String(status),
30
+ json: async () => route.body,
31
+ };
32
+ };
33
+
34
+ return { impl: vi.fn(impl) as unknown as UmamiFetch, calls };
35
+ }
36
+
37
+ const CREDENTIALS = {
38
+ apiUrl: "https://umami.example.com/",
39
+ username: "ops@example.com",
40
+ password: "secret",
41
+ };
42
+
43
+ const LOGIN: Route = { body: { token: "tok", user: { id: "u1", username: "ops" } } };
44
+
45
+ describe("UmamiReportingClient auth", () => {
46
+ it("mints a token once and reuses it across requests", async () => {
47
+ const { impl, calls } = makeFetch({
48
+ "/api/auth/login": LOGIN,
49
+ "/active": { body: { visitors: 3 } },
50
+ });
51
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
52
+
53
+ await client.getActive("site-1");
54
+ await client.getActive("site-1");
55
+
56
+ const logins = calls.filter((c) => c.url.includes("/api/auth/login"));
57
+ expect(logins).toHaveLength(1);
58
+ });
59
+
60
+ it("strips a trailing slash from the api url", async () => {
61
+ const { impl, calls } = makeFetch({
62
+ "/api/auth/login": LOGIN,
63
+ "/active": { body: { visitors: 0 } },
64
+ });
65
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
66
+
67
+ await client.getActive("site-1");
68
+
69
+ expect(calls[0].url).toBe("https://umami.example.com/api/auth/login");
70
+ });
71
+
72
+ it("sends both username and email so either login mode matches", async () => {
73
+ const { impl, calls } = makeFetch({
74
+ "/api/auth/login": LOGIN,
75
+ "/active": { body: { visitors: 0 } },
76
+ });
77
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
78
+
79
+ await client.getActive("site-1");
80
+
81
+ expect(JSON.parse(calls[0].body ?? "{}")).toEqual({
82
+ username: "ops@example.com",
83
+ email: "ops@example.com",
84
+ password: "secret",
85
+ });
86
+ });
87
+
88
+ it("attaches Cloudflare Access headers to every call when a service token is given", async () => {
89
+ const { impl, calls } = makeFetch({
90
+ "/api/auth/login": LOGIN,
91
+ "/active": { body: { visitors: 1 } },
92
+ });
93
+ const client = new UmamiReportingClient({
94
+ ...CREDENTIALS,
95
+ accessClientId: "cf-id",
96
+ accessClientSecret: "cf-secret",
97
+ fetchImpl: impl,
98
+ });
99
+
100
+ await client.getActive("site-1");
101
+
102
+ for (const call of calls) {
103
+ expect(call.headers["CF-Access-Client-Id"]).toBe("cf-id");
104
+ expect(call.headers["CF-Access-Client-Secret"]).toBe("cf-secret");
105
+ }
106
+ });
107
+
108
+ it("omits the Access headers entirely when no service token is given", async () => {
109
+ const { impl, calls } = makeFetch({
110
+ "/api/auth/login": LOGIN,
111
+ "/active": { body: { visitors: 1 } },
112
+ });
113
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
114
+
115
+ await client.getActive("site-1");
116
+
117
+ expect(calls[0].headers["CF-Access-Client-Id"]).toBeUndefined();
118
+ });
119
+
120
+ it("throws with the status when the login is rejected", async () => {
121
+ const { impl } = makeFetch({ "/api/auth/login": { status: 401, body: {} } });
122
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
123
+
124
+ await expect(client.getActive("site-1")).rejects.toThrow(/Umami auth failed: 401/);
125
+ });
126
+
127
+ it("drops the cached token when a request comes back 401", async () => {
128
+ const { impl, calls } = makeFetch({
129
+ "/api/auth/login": LOGIN,
130
+ "/active": { status: 401, body: {} },
131
+ });
132
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
133
+
134
+ await expect(client.getActive("site-1")).rejects.toThrow(/token expired/);
135
+ await expect(client.getActive("site-1")).rejects.toThrow(/token expired/);
136
+
137
+ // Two logins, because the first 401 invalidated the token it had cached.
138
+ expect(calls.filter((c) => c.url.includes("/api/auth/login"))).toHaveLength(2);
139
+ });
140
+ });
141
+
142
+ describe("UmamiReportingClient websites", () => {
143
+ it("merges personal and team websites, deduped by id", async () => {
144
+ const { impl } = makeFetch({
145
+ "/api/auth/login": LOGIN,
146
+ "/api/teams/team-1/websites": {
147
+ body: { data: [{ id: "b", name: "B", domain: "b.com" }], count: 1, page: 1, pageSize: 150 },
148
+ },
149
+ "/api/websites?": {
150
+ body: {
151
+ data: [
152
+ { id: "a", name: "A", domain: "a.com" },
153
+ { id: "b", name: "B", domain: "b.com" },
154
+ ],
155
+ count: 2,
156
+ page: 1,
157
+ pageSize: 150,
158
+ },
159
+ },
160
+ });
161
+ const client = new UmamiReportingClient({
162
+ ...CREDENTIALS,
163
+ teamId: "team-1",
164
+ fetchImpl: impl,
165
+ });
166
+
167
+ const websites = await client.getWebsites();
168
+
169
+ expect(websites.map((w) => w.id)).toEqual(["a", "b"]);
170
+ });
171
+
172
+ it("caches the website list rather than re-listing on every lookup", async () => {
173
+ const { impl, calls } = makeFetch({
174
+ "/api/auth/login": LOGIN,
175
+ "/api/websites?": {
176
+ body: { data: [{ id: "a", name: "A", domain: "a.com" }], count: 1, page: 1, pageSize: 150 },
177
+ },
178
+ });
179
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
180
+
181
+ await client.getWebsites();
182
+ await client.getWebsites();
183
+
184
+ expect(calls.filter((c) => c.url.includes("/api/websites?"))).toHaveLength(1);
185
+ });
186
+
187
+ it("resolves a site by uuid without listing anything", async () => {
188
+ const { impl, calls } = makeFetch({ "/api/auth/login": LOGIN });
189
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
190
+
191
+ const id = await client.resolveSiteId("cff58573-0126-4ecc-a054-aaeabce758bd");
192
+
193
+ expect(id).toBe("cff58573-0126-4ecc-a054-aaeabce758bd");
194
+ expect(calls).toHaveLength(0);
195
+ });
196
+
197
+ it("resolves a site by exact domain, then by partial name", async () => {
198
+ const { impl } = makeFetch({
199
+ "/api/auth/login": LOGIN,
200
+ "/api/websites?": {
201
+ body: {
202
+ data: [
203
+ { id: "a", name: "UpSys Consulting", domain: "upsys-consulting.com" },
204
+ { id: "b", name: "Save to ClickUp", domain: "savetoclickup.com" },
205
+ ],
206
+ count: 2,
207
+ page: 1,
208
+ pageSize: 150,
209
+ },
210
+ },
211
+ });
212
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
213
+
214
+ expect(await client.resolveSiteId("savetoclickup.com")).toBe("b");
215
+ expect(await client.resolveSiteId("upsys")).toBe("a");
216
+ await expect(client.resolveSiteId("nothing")).rejects.toThrow(/No website found/);
217
+ });
218
+
219
+ it("creates a website, sends the body, and drops the cached list", async () => {
220
+ const listed = {
221
+ body: { data: [{ id: "a", name: "A", domain: "a.com" }], count: 1, page: 1, pageSize: 150 },
222
+ };
223
+ const { impl, calls } = makeFetch({
224
+ "/api/auth/login": LOGIN,
225
+ "/api/websites?": listed,
226
+ "/api/websites": { body: { id: "b", name: "B", domain: "b.com" } },
227
+ });
228
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
229
+
230
+ await client.getWebsites();
231
+ const created = await client.createWebsite({ name: "B", domain: "b.com" });
232
+ await client.getWebsites();
233
+
234
+ expect(created.id).toBe("b");
235
+ const posts = calls.filter((c) => c.method === "POST" && c.url.includes("/api/websites"));
236
+ expect(posts).toHaveLength(1);
237
+ expect(JSON.parse(posts[0]?.body ?? "{}")).toEqual({ name: "B", domain: "b.com" });
238
+ // The list is re-fetched, because the create invalidated the cache.
239
+ expect(calls.filter((c) => c.url.includes("/api/websites?"))).toHaveLength(2);
240
+ });
241
+
242
+ it("only sends teamId when one is given", async () => {
243
+ const { impl, calls } = makeFetch({
244
+ "/api/auth/login": LOGIN,
245
+ "/api/websites": { body: { id: "b", name: "B", domain: "b.com" } },
246
+ });
247
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
248
+
249
+ await client.createWebsite({ name: "B", domain: "b.com", teamId: "t1" });
250
+
251
+ // The login is a POST too, so the URL is what separates them.
252
+ const posts = calls.filter((c) => c.method === "POST" && c.url.includes("/api/websites"));
253
+ expect(JSON.parse(posts[0]?.body ?? "{}")).toEqual({
254
+ name: "B",
255
+ domain: "b.com",
256
+ teamId: "t1",
257
+ });
258
+ });
259
+
260
+ it("deletes a website and drops the cached list", async () => {
261
+ const listed = {
262
+ body: { data: [{ id: "a", name: "A", domain: "a.com" }], count: 1, page: 1, pageSize: 150 },
263
+ };
264
+ const { impl, calls } = makeFetch({
265
+ "/api/auth/login": LOGIN,
266
+ "/api/websites?": listed,
267
+ "/api/websites/a": { body: "ok" },
268
+ });
269
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
270
+
271
+ await client.getWebsites();
272
+ await client.deleteWebsite("a");
273
+ await client.getWebsites();
274
+
275
+ const deletes = calls.filter((c) => c.method === "DELETE");
276
+ expect(deletes).toHaveLength(1);
277
+ expect(deletes[0]?.url).toContain("/api/websites/a");
278
+ // The list is re-fetched because the cache the delete invalidated is gone.
279
+ expect(calls.filter((c) => c.url.includes("/api/websites?"))).toHaveLength(2);
280
+ });
281
+ });
282
+
283
+ describe("UmamiReportingClient reads", () => {
284
+ it("returns stats with the comparison window Umami actually sends", async () => {
285
+ const { impl } = makeFetch({
286
+ "/api/auth/login": LOGIN,
287
+ "/stats": {
288
+ body: {
289
+ pageviews: 37,
290
+ visitors: 31,
291
+ visits: 31,
292
+ bounces: 28,
293
+ totaltime: 147,
294
+ comparison: {
295
+ pageviews: 330,
296
+ visitors: 263,
297
+ visits: 268,
298
+ bounces: 242,
299
+ totaltime: 6264,
300
+ },
301
+ },
302
+ },
303
+ });
304
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
305
+
306
+ const stats = await client.getStats("site-1", 0, 1);
307
+
308
+ expect(stats.pageviews).toBe(37);
309
+ expect(stats.comparison?.pageviews).toBe(330);
310
+ });
311
+
312
+ it("url-encodes the timezone on a series read", async () => {
313
+ const { impl, calls } = makeFetch({
314
+ "/api/auth/login": LOGIN,
315
+ "/pageviews": { body: { pageviews: [], sessions: [] } },
316
+ });
317
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
318
+
319
+ await client.getPageviews("site-1", 0, 1, "day", "Europe/Lisbon");
320
+
321
+ const call = calls.find((c) => c.url.includes("/pageviews"));
322
+ expect(call?.url).toContain("timezone=Europe%2FLisbon");
323
+ });
324
+
325
+ it("coerces the string counters Umami returns on expanded metrics", async () => {
326
+ const { impl } = makeFetch({
327
+ "/api/auth/login": LOGIN,
328
+ "/metrics/expanded": {
329
+ body: [
330
+ {
331
+ name: "/pricing",
332
+ pageviews: "42",
333
+ visitors: 30,
334
+ visits: 31,
335
+ bounces: 2,
336
+ totaltime: "90",
337
+ },
338
+ ],
339
+ },
340
+ });
341
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
342
+
343
+ const [row] = await client.getExpandedMetrics("site-1", "path", 0, 1);
344
+
345
+ expect(row.pageviews).toBe(42);
346
+ expect(row.totaltime).toBe(90);
347
+ });
348
+
349
+ it("surfaces the failing path in a non-401 API error", async () => {
350
+ const { impl } = makeFetch({
351
+ "/api/auth/login": LOGIN,
352
+ "/stats": { status: 500, body: {} },
353
+ });
354
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
355
+
356
+ await expect(client.getStats("site-1", 0, 1)).rejects.toThrow(/Umami API error: 500/);
357
+ });
358
+ });
359
+
360
+ describe("UmamiReportingClient event data", () => {
361
+ it("url-encodes an event name that carries a slash or a space", async () => {
362
+ const { impl, calls } = makeFetch({
363
+ "/api/auth/login": LOGIN,
364
+ "/event-data/values": { body: [{ value: "49.00", total: 2 }] },
365
+ });
366
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
367
+
368
+ const rows = await client.getEventDataValues("site-1", 0, 1, "buy click/pro", "revenue");
369
+
370
+ const call = calls.find((c) => c.url.includes("/event-data/values"));
371
+ expect(call?.url).toContain("event=buy+click%2Fpro");
372
+ // The server validates this name and 400s on `property`, which is what the
373
+ // docs call it. Asserted so the docs cannot talk the next pass out of it.
374
+ expect(call?.url).toContain("propertyName=revenue");
375
+ expect(rows).toEqual([{ value: "49.00", total: 2 }]);
376
+ });
377
+
378
+ it("lists the recorded event fields", async () => {
379
+ const { impl } = makeFetch({
380
+ "/api/auth/login": LOGIN,
381
+ "/event-data/stats": {
382
+ body: [{ eventName: "buy_click", fieldName: "revenue", total: 8 }],
383
+ },
384
+ });
385
+ const client = new UmamiReportingClient({ ...CREDENTIALS, fetchImpl: impl });
386
+
387
+ expect(await client.getEventDataStats("site-1", 0, 1)).toEqual([
388
+ { eventName: "buy_click", fieldName: "revenue", total: 8 },
389
+ ]);
390
+ });
391
+ });