hanzio 2.2.0 → 3.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 (116) hide show
  1. package/README.md +133 -7
  2. package/dist/api-wrapper/connection-auth.d.ts +61 -0
  3. package/dist/cli/fields.d.ts +30 -0
  4. package/dist/cli/index.d.ts +122 -0
  5. package/dist/format/index.d.ts +91 -0
  6. package/dist/format/phone.d.ts +19 -0
  7. package/dist/http/index.d.ts +14 -0
  8. package/dist/i18n/catalog.d.ts +47 -0
  9. package/dist/i18n/check.d.ts +45 -0
  10. package/dist/i18n/cli.d.ts +2 -0
  11. package/dist/i18n/content-rules.d.ts +55 -0
  12. package/dist/i18n/i18next.d.ts +39 -0
  13. package/dist/i18n/index.d.ts +16 -1
  14. package/dist/i18n/locales.d.ts +62 -0
  15. package/dist/i18n/vite.d.ts +16 -0
  16. package/dist/i18n/vue.d.ts +93 -0
  17. package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
  18. package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
  19. package/dist/index-5n2e326q.js +409 -0
  20. package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
  21. package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
  22. package/dist/index-6ragggbs.js +473 -0
  23. package/dist/index-mq2qgc20.js +116 -0
  24. package/dist/index-najbyzpr.js +99 -0
  25. package/dist/index-ww09bm49.js +250 -0
  26. package/dist/iso/countries.d.ts +262 -0
  27. package/dist/iso/currencies.d.ts +165 -0
  28. package/dist/iso/index.d.ts +4 -0
  29. package/dist/iso/languages.d.ts +195 -0
  30. package/dist/iso/phone-language.d.ts +5 -0
  31. package/dist/promise/index.d.ts +31 -0
  32. package/dist/redis/client.d.ts +49 -0
  33. package/dist/redis/concurrency-limit.d.ts +63 -0
  34. package/dist/redis/index.d.ts +3 -0
  35. package/dist/redis/lua-script.d.ts +13 -0
  36. package/dist/secrets/infisical-identity-file.d.ts +12 -0
  37. package/dist/src/api-wrapper/connection-auth.js +82 -0
  38. package/dist/src/api-wrapper/index.js +1 -1
  39. package/dist/src/cli/index.js +10 -0
  40. package/dist/src/format/index.js +79 -2
  41. package/dist/src/format/phone.js +21 -0
  42. package/dist/src/http/index.js +69 -0
  43. package/dist/src/i18n/check.js +15 -0
  44. package/dist/src/i18n/cli.js +71 -0
  45. package/dist/src/i18n/i18next.js +48 -0
  46. package/dist/src/i18n/index.js +16 -34
  47. package/dist/src/i18n/vite.js +25 -0
  48. package/dist/src/i18n/vue.js +51 -0
  49. package/dist/src/index.js +22 -6
  50. package/dist/src/iso/index.js +655 -0
  51. package/dist/src/redis/index.js +577 -0
  52. package/dist/src/secrets/index.js +78 -35
  53. package/dist/src/sitemap/index.js +1 -1
  54. package/dist/src/storage/index.js +8 -117
  55. package/dist/src/testing/index.js +348 -0
  56. package/dist/src/zod/index.js +20 -253
  57. package/dist/string/index.d.ts +21 -0
  58. package/dist/testing/connection-auth-contract.d.ts +43 -0
  59. package/dist/testing/helpers.d.ts +26 -0
  60. package/dist/testing/index.d.ts +5 -0
  61. package/dist/testing/module-mocker.d.ts +30 -0
  62. package/dist/testing/prisma.d.ts +30 -0
  63. package/dist/testing/window.d.ts +18 -0
  64. package/dist/typedswitch/index.d.ts +55 -34
  65. package/dist/zod/index.d.ts +10 -0
  66. package/dist/zod/introspect.d.ts +2 -1
  67. package/package.json +97 -2
  68. package/src/api-wrapper/README.md +57 -2
  69. package/src/api-wrapper/SKILL.md +256 -0
  70. package/src/api-wrapper/connection-auth.ts +142 -0
  71. package/src/cli/README.md +220 -0
  72. package/src/cli/fields.ts +213 -0
  73. package/src/cli/index.ts +532 -0
  74. package/src/format/README.md +82 -2
  75. package/src/format/index.ts +148 -0
  76. package/src/format/phone.ts +43 -0
  77. package/src/http/README.md +25 -0
  78. package/src/http/index.ts +94 -0
  79. package/src/i18n/README.md +206 -3
  80. package/src/i18n/catalog.ts +139 -0
  81. package/src/i18n/check.ts +376 -0
  82. package/src/i18n/cli.ts +90 -0
  83. package/src/i18n/content-rules.ts +179 -0
  84. package/src/i18n/i18next.ts +89 -0
  85. package/src/i18n/index.ts +34 -0
  86. package/src/i18n/locales.ts +135 -0
  87. package/src/i18n/vite.ts +40 -0
  88. package/src/i18n/vue.ts +82 -0
  89. package/src/index.ts +1 -1
  90. package/src/iso/README.md +44 -0
  91. package/src/iso/countries.ts +272 -0
  92. package/src/iso/currencies.ts +172 -0
  93. package/src/iso/index.ts +8 -0
  94. package/src/iso/languages.ts +208 -0
  95. package/src/iso/phone-language.ts +46 -0
  96. package/src/promise/index.ts +100 -0
  97. package/src/redis/README.md +112 -0
  98. package/src/redis/client.ts +239 -0
  99. package/src/redis/concurrency-limit.ts +591 -0
  100. package/src/redis/index.ts +15 -0
  101. package/src/redis/lua-script.ts +43 -0
  102. package/src/secrets/README.md +5 -1
  103. package/src/secrets/infisical-identity-file.ts +69 -0
  104. package/src/secrets/infisical.ts +18 -3
  105. package/src/string/index.ts +59 -0
  106. package/src/testing/README.md +119 -0
  107. package/src/testing/connection-auth-contract.ts +303 -0
  108. package/src/testing/helpers.ts +41 -0
  109. package/src/testing/index.ts +12 -0
  110. package/src/testing/module-mocker.ts +50 -0
  111. package/src/testing/prisma.ts +78 -0
  112. package/src/testing/window.ts +119 -0
  113. package/src/typedswitch/README.md +55 -39
  114. package/src/typedswitch/index.ts +97 -83
  115. package/src/zod/index.ts +20 -0
  116. package/src/zod/introspect.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HttpResponseError2,
3
3
  createApiClient2
4
- } from "../../index-zbj9s3s3.js";
4
+ } from "../../index-2kz392vw.js";
5
5
  import {
6
6
  getViteDefine2,
7
7
  viteSecretSetPlugin2
@@ -19,7 +19,7 @@ import {
19
19
  } from "../../index-ktynw3vs.js";
20
20
 
21
21
  // src/secrets/infisical.ts
22
- import { z as z2 } from "zod";
22
+ import { z as z3 } from "zod";
23
23
 
24
24
  // src/secrets/infisical-cli.ts
25
25
  async function readInfisicalCliToken(options) {
@@ -102,12 +102,53 @@ function validateInfisicalOrganizationId(organizationId) {
102
102
  }
103
103
  }
104
104
 
105
- // src/secrets/infisical-session.ts
105
+ // src/secrets/infisical-identity-file.ts
106
106
  import { z } from "zod";
107
- var sessionClaimsSchema = z.object({
108
- organizationId: z.string().optional(),
109
- subOrganizationId: z.string().optional(),
110
- exp: z.number().finite().optional()
107
+ var identityFileSchema = z.object({
108
+ clientId: z.string().min(1),
109
+ clientSecret: z.string().min(1),
110
+ siteUrl: z.string().url()
111
+ });
112
+ async function readInfisicalIdentityFile(env, siteUrl) {
113
+ const modules = await Promise.all([
114
+ import("node:fs/promises"),
115
+ import("node:os"),
116
+ import("node:path")
117
+ ]).catch(() => {
118
+ return;
119
+ });
120
+ if (!modules) {
121
+ return;
122
+ }
123
+ const [fs, os, path] = modules;
124
+ const file = env?.INFISICAL_UNIVERSAL_AUTH_FILE || path.join(os.homedir(), ".infisical", "universal-auth.json");
125
+ let text;
126
+ try {
127
+ text = await fs.readFile(file, "utf8");
128
+ } catch (error) {
129
+ if (error.code === "ENOENT") {
130
+ return;
131
+ }
132
+ throw new SecretLoadError2("CONFIGURATION", `The Infisical identity file ${file} could not be read.`);
133
+ }
134
+ let identity;
135
+ try {
136
+ identity = identityFileSchema.parse(JSON.parse(text));
137
+ } catch {
138
+ throw new SecretLoadError2("CONFIGURATION", `The Infisical identity file ${file} is not valid; it needs clientId, clientSecret and siteUrl.`);
139
+ }
140
+ if (new URL(identity.siteUrl).origin !== new URL(siteUrl).origin) {
141
+ return;
142
+ }
143
+ return { clientId: identity.clientId, clientSecret: identity.clientSecret };
144
+ }
145
+
146
+ // src/secrets/infisical-session.ts
147
+ import { z as z2 } from "zod";
148
+ var sessionClaimsSchema = z2.object({
149
+ organizationId: z2.string().optional(),
150
+ subOrganizationId: z2.string().optional(),
151
+ exp: z2.number().finite().optional()
111
152
  });
112
153
  function parseInfisicalSessionToken(token) {
113
154
  let claims;
@@ -130,13 +171,13 @@ function parseInfisicalSessionToken(token) {
130
171
  // src/secrets/infisical.ts
131
172
  var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
132
173
  var defaultFetch = globalThis.fetch;
133
- var infisicalAuthResponseSchema = z2.object({ accessToken: z2.string() });
134
- var infisicalSecretSchema = z2.object({
135
- secretKey: z2.string(),
136
- secretValue: z2.string()
174
+ var infisicalAuthResponseSchema = z3.object({ accessToken: z3.string() });
175
+ var infisicalSecretSchema = z3.object({
176
+ secretKey: z3.string(),
177
+ secretValue: z3.string()
137
178
  });
138
- var infisicalSecretsResponseSchema = z2.object({
139
- secrets: z2.array(infisicalSecretSchema)
179
+ var infisicalSecretsResponseSchema = z3.object({
180
+ secrets: z3.array(infisicalSecretSchema)
140
181
  });
141
182
  async function fetchInfisicalSecrets(options) {
142
183
  validateInfisicalScope(options);
@@ -165,18 +206,20 @@ async function fetchInfisicalSecrets(options) {
165
206
  async function loadInfisicalSecrets(options) {
166
207
  validateInfisicalScope(options);
167
208
  const env = getProcessEnvironment();
168
- const clientId = env?.[options.clientIdEnvKey];
169
- const clientSecret = env?.[options.clientSecretEnvKey];
170
209
  const mode = options.auth ?? "auto";
171
210
  if (!["auto", "http", "cli"].includes(mode)) {
172
211
  throw new SecretLoadError2("CONFIGURATION", "Invalid Infisical auth mode.");
173
212
  }
213
+ const fromEnv = Boolean(env?.[options.clientIdEnvKey] || env?.[options.clientSecretEnvKey]);
214
+ const file = mode === "cli" || fromEnv ? undefined : await readInfisicalIdentityFile(env, options.siteUrl);
215
+ const clientId = fromEnv ? env?.[options.clientIdEnvKey] : file?.clientId;
216
+ const clientSecret = fromEnv ? env?.[options.clientSecretEnvKey] : file?.clientSecret;
174
217
  const useHttp = mode === "http" || mode === "auto" && Boolean(clientId && clientSecret);
175
218
  if (useHttp) {
176
219
  if (!clientId || !clientSecret) {
177
220
  throw new SecretLoadError2("AUTHENTICATION", "Infisical HTTP authentication requires both client credentials.");
178
221
  }
179
- options.reportSource?.("Infisical HTTP");
222
+ options.reportSource?.(file ? "Infisical HTTP (identity file)" : "Infisical HTTP");
180
223
  return fetchInfisicalSecrets({ ...options, clientId, clientSecret });
181
224
  }
182
225
  options.reportSource?.(`Infisical CLI session (organization=${options.organizationId} project=${options.projectId} environment=${options.environment})`);
@@ -256,23 +299,23 @@ function createInfisicalApiClient(siteUrl) {
256
299
  selectOrganization: {
257
300
  method: "POST",
258
301
  path: "/api/v3/auth/select-organization",
259
- reqHeadersSchema: z2.object({
260
- Authorization: z2.string(),
261
- "User-Agent": z2.string()
302
+ reqHeadersSchema: z3.object({
303
+ Authorization: z3.string(),
304
+ "User-Agent": z3.string()
262
305
  }),
263
- reqBodySchema: z2.object({ organizationId: z2.string() }),
264
- resSchema: z2.object({
265
- token: z2.string().min(1),
266
- isMfaEnabled: z2.boolean().optional()
306
+ reqBodySchema: z3.object({ organizationId: z3.string() }),
307
+ resSchema: z3.object({
308
+ token: z3.string().min(1),
309
+ isMfaEnabled: z3.boolean().optional()
267
310
  }),
268
311
  reqBodyFormat: "json"
269
312
  },
270
313
  login: {
271
314
  method: "POST",
272
315
  path: "/api/v1/auth/universal-auth/login",
273
- reqBodySchema: z2.object({
274
- clientId: z2.string(),
275
- clientSecret: z2.string()
316
+ reqBodySchema: z3.object({
317
+ clientId: z3.string(),
318
+ clientSecret: z3.string()
276
319
  }),
277
320
  resSchema: infisicalAuthResponseSchema,
278
321
  reqBodyFormat: "json"
@@ -280,16 +323,16 @@ function createInfisicalApiClient(siteUrl) {
280
323
  listSecrets: {
281
324
  method: "GET",
282
325
  path: "/api/v4/secrets",
283
- reqHeadersSchema: z2.object({
284
- Authorization: z2.string()
326
+ reqHeadersSchema: z3.object({
327
+ Authorization: z3.string()
285
328
  }),
286
- reqQuerySchema: z2.object({
287
- projectId: z2.string(),
288
- environment: z2.string(),
289
- secretPath: z2.string(),
290
- viewSecretValue: z2.boolean(),
291
- expandSecretReferences: z2.boolean(),
292
- includeImports: z2.boolean()
329
+ reqQuerySchema: z3.object({
330
+ projectId: z3.string(),
331
+ environment: z3.string(),
332
+ secretPath: z3.string(),
333
+ viewSecretValue: z3.boolean(),
334
+ expandSecretReferences: z3.boolean(),
335
+ includeImports: z3.boolean()
293
336
  }),
294
337
  resSchema: infisicalSecretsResponseSchema
295
338
  }
@@ -3,7 +3,7 @@ import {
3
3
  } from "../../index-pwt0dz63.js";
4
4
  import {
5
5
  escapeRegExp2
6
- } from "../../index-kj1phgrx.js";
6
+ } from "../../index-29mmh343.js";
7
7
  import {
8
8
  normalizeDomain2
9
9
  } from "../../index-mnkcfyx8.js";
@@ -1,119 +1,10 @@
1
- // src/storage/index.ts
2
- function createMemoryStorage() {
3
- const map = new Map;
4
- return {
5
- getItem: (key) => map.get(key) ?? null,
6
- setItem: (key, value) => {
7
- map.set(key, String(value));
8
- },
9
- removeItem: (key) => {
10
- map.delete(key);
11
- }
12
- };
13
- }
14
- var sharedMemoryStorage;
15
- function defaultStorage() {
16
- try {
17
- const storage = globalThis.localStorage;
18
- if (storage)
19
- return storage;
20
- } catch {}
21
- sharedMemoryStorage ??= createMemoryStorage();
22
- return sharedMemoryStorage;
23
- }
24
- function createCodec(options) {
25
- const {
26
- fallback,
27
- parse = (raw) => raw,
28
- serialize = JSON.stringify,
29
- deserialize = JSON.parse
30
- } = options;
31
- return {
32
- serialize,
33
- decode(raw) {
34
- if (raw === null)
35
- return fallback;
36
- try {
37
- return parse(deserialize(raw));
38
- } catch {
39
- return fallback;
40
- }
41
- }
42
- };
43
- }
44
- function createStorageItem(options) {
45
- const { key, fallback, storage = defaultStorage() } = options;
46
- const { serialize, decode } = createCodec(options);
47
- let mirror;
48
- const write = (value, action) => {
49
- try {
50
- action();
51
- mirror = undefined;
52
- } catch {
53
- mirror = { value };
54
- }
55
- };
56
- const get = () => {
57
- if (mirror)
58
- return mirror.value;
59
- try {
60
- return decode(storage.getItem(key));
61
- } catch {
62
- return fallback;
63
- }
64
- };
65
- return {
66
- key,
67
- get,
68
- set: (value) => write(value, () => storage.setItem(key, serialize(value))),
69
- remove: () => write(fallback, () => storage.removeItem(key)),
70
- subscribe(listener) {
71
- const target = globalThis;
72
- if (!target.addEventListener || !target.removeEventListener) {
73
- return () => {};
74
- }
75
- const onStorage = (event) => {
76
- if (event.storageArea !== storage)
77
- return;
78
- if (event.key !== key && event.key !== null)
79
- return;
80
- mirror = undefined;
81
- listener(get());
82
- };
83
- target.addEventListener("storage", onStorage);
84
- return () => target.removeEventListener?.("storage", onStorage);
85
- }
86
- };
87
- }
88
- function createAsyncStorageItem(options) {
89
- const { key, fallback, storage } = options;
90
- const { serialize, decode } = createCodec(options);
91
- let mirror;
92
- const write = async (value, action) => {
93
- try {
94
- await action();
95
- mirror = undefined;
96
- } catch {
97
- mirror = { value };
98
- }
99
- };
100
- return {
101
- key,
102
- async get() {
103
- if (mirror)
104
- return mirror.value;
105
- try {
106
- return decode(await storage.getItem(key));
107
- } catch {
108
- return fallback;
109
- }
110
- },
111
- set: (value) => write(value, () => storage.setItem(key, serialize(value))),
112
- remove: () => write(fallback, () => storage.removeItem(key))
113
- };
114
- }
1
+ import {
2
+ createMemoryStorage2,
3
+ createStorageItem2,
4
+ createAsyncStorageItem2
5
+ } from "../../index-mq2qgc20.js";
115
6
  export {
116
- createAsyncStorageItem,
117
- createMemoryStorage,
118
- createStorageItem
7
+ createAsyncStorageItem2 as createAsyncStorageItem,
8
+ createMemoryStorage2 as createMemoryStorage,
9
+ createStorageItem2 as createStorageItem
119
10
  };
@@ -0,0 +1,348 @@
1
+ // src/testing/connection-auth-contract.ts
2
+ var cloneResponse = (response) => response.clone();
3
+ async function connectionAuthContract(input) {
4
+ const { beforeEach, describe, expect, test } = await import("bun:test");
5
+ describe("shared authentication contract", () => {
6
+ beforeEach(input.reset);
7
+ function provider(respond) {
8
+ let exchanges = 0;
9
+ const usedTokens = [];
10
+ const fetchImpl = async (url, init) => {
11
+ const login = input.isLogin(String(url));
12
+ if (login)
13
+ exchanges += 1;
14
+ else
15
+ usedTokens.push(new Headers(init?.headers).get(input.tokenHeader));
16
+ return respond(login, init ?? {});
17
+ };
18
+ return { fetchImpl, usedTokens, exchanges: () => exchanges };
19
+ }
20
+ test.each(input.identityChanges)("%s changes authentication identity", async (variant) => {
21
+ const fake = provider((login) => login ? input.loginResponse(`token-${fake.exchanges()}`) : input.readResponse());
22
+ await input.create(fake.fetchImpl).read();
23
+ await input.create(fake.fetchImpl, variant).read();
24
+ expect(fake.exchanges()).toBe(2);
25
+ expect(fake.usedTokens[1]).not.toBe(fake.usedTokens[0]);
26
+ });
27
+ test("non-auth settings reuse completed authentication across instances", async () => {
28
+ const fake = provider((login) => login ? input.loginResponse("same") : input.readResponse());
29
+ await input.create(fake.fetchImpl).read();
30
+ await input.create(fake.fetchImpl, "settings").read();
31
+ expect(fake.exchanges()).toBe(1);
32
+ });
33
+ test("unrelated connections proceed while another login is blocked", async () => {
34
+ const pending = Promise.withResolvers();
35
+ const fake = provider((login) => {
36
+ if (!login)
37
+ return input.readResponse();
38
+ return fake.exchanges() === 1 ? pending.promise : input.loginResponse("independent");
39
+ });
40
+ const blocked = input.create(fake.fetchImpl).read();
41
+ await input.create(fake.fetchImpl, "connection").read();
42
+ expect(fake.exchanges()).toBe(2);
43
+ pending.resolve(input.loginResponse("blocked"));
44
+ await blocked;
45
+ });
46
+ test("failed shared refresh is evicted and a later attempt recovers", async () => {
47
+ const pending = Promise.withResolvers();
48
+ const fake = provider((login) => login ? pending.promise.then(cloneResponse) : input.readResponse());
49
+ const results = Promise.allSettled(Array.from({ length: 3 }, () => input.create(fake.fetchImpl).read()));
50
+ pending.reject(new Error("provider unavailable"));
51
+ expect((await results).map((result) => result.status)).toEqual([
52
+ "rejected",
53
+ "rejected",
54
+ "rejected"
55
+ ]);
56
+ expect(fake.exchanges()).toBe(1);
57
+ const recovered = provider((login) => login ? input.loginResponse("recovered") : input.readResponse());
58
+ await input.create(recovered.fetchImpl).read();
59
+ expect(recovered.exchanges()).toBe(1);
60
+ });
61
+ test("simultaneous 401s share one refresh, and a delayed 401 cannot evict it", async () => {
62
+ const rejected = Array.from({ length: 3 }, () => Promise.withResolvers());
63
+ const requestsStarted = Promise.withResolvers();
64
+ const refresh = Promise.withResolvers();
65
+ let oldRequests = 0;
66
+ const fake = provider((login, init) => {
67
+ if (login)
68
+ return fake.exchanges() === 1 ? input.loginResponse("A") : refresh.promise.then(cloneResponse);
69
+ const header = new Headers(init.headers).get(input.tokenHeader);
70
+ if (header === (input.tokenHeader === "authorization" ? "Bearer A" : "A")) {
71
+ const response = rejected[oldRequests++];
72
+ if (oldRequests === 3)
73
+ requestsStarted.resolve();
74
+ return response.promise;
75
+ }
76
+ return input.readResponse();
77
+ });
78
+ await input.create(fake.fetchImpl).login();
79
+ const reads = Array.from({ length: 3 }, () => input.create(fake.fetchImpl).read());
80
+ await requestsStarted.promise;
81
+ rejected[0].resolve(new Response("", { status: 401 }));
82
+ rejected[1].resolve(new Response("", { status: 401 }));
83
+ refresh.resolve(input.loginResponse("B"));
84
+ await Promise.all(reads.slice(0, 2));
85
+ rejected[2].resolve(new Response("", { status: 401 }));
86
+ await reads[2];
87
+ expect(fake.exchanges()).toBe(2);
88
+ expect(fake.usedTokens).toHaveLength(6);
89
+ expect(fake.usedTokens.slice(3)).toEqual(Array(3).fill(input.tokenHeader === "authorization" ? "Bearer B" : "B"));
90
+ });
91
+ test("a delayed 401 cannot evict a new generation with identical token bytes", async () => {
92
+ const rejected = Promise.withResolvers();
93
+ const started = Promise.withResolvers();
94
+ let reads = 0;
95
+ const fake = provider((login) => {
96
+ if (login)
97
+ return input.loginResponse("same");
98
+ reads += 1;
99
+ if (reads === 1) {
100
+ started.resolve();
101
+ return rejected.promise;
102
+ }
103
+ return input.readResponse();
104
+ });
105
+ await input.create(fake.fetchImpl).login();
106
+ const oldRead = input.create(fake.fetchImpl).read();
107
+ await started.promise;
108
+ await input.create(fake.fetchImpl).login();
109
+ rejected.resolve(new Response("", { status: 401 }));
110
+ await oldRead;
111
+ expect(fake.exchanges()).toBe(2);
112
+ expect(reads).toBe(2);
113
+ });
114
+ test("failed fresh authentication cannot fall back to the previous completed token", async () => {
115
+ const fake = provider((login) => {
116
+ if (!login)
117
+ return input.readResponse();
118
+ if (fake.exchanges() === 2)
119
+ throw new Error("credentials rejected");
120
+ return input.loginResponse("valid");
121
+ });
122
+ await input.create(fake.fetchImpl).read();
123
+ await expect(input.create(fake.fetchImpl).login()).rejects.toThrow("credentials rejected");
124
+ await input.create(fake.fetchImpl).read();
125
+ expect(fake.exchanges()).toBe(3);
126
+ });
127
+ test("a late old login cannot replace rotated authentication", async () => {
128
+ const pending = Promise.withResolvers();
129
+ const fake = provider((login) => {
130
+ if (!login)
131
+ return input.readResponse();
132
+ return fake.exchanges() === 1 ? pending.promise : input.loginResponse("new");
133
+ });
134
+ const old = input.create(fake.fetchImpl).read();
135
+ await input.create(fake.fetchImpl, "secret").read();
136
+ pending.resolve(input.loginResponse("old"));
137
+ await old;
138
+ await input.create(fake.fetchImpl, "secret").read();
139
+ expect(fake.exchanges()).toBe(2);
140
+ expect(fake.usedTokens.at(-1)).toBe(input.tokenHeader === "authorization" ? "Bearer new" : "new");
141
+ });
142
+ test("fresh logins bypass completed tokens and coalesce identical pending checks", async () => {
143
+ const pending = Promise.withResolvers();
144
+ const fake = provider((login) => {
145
+ if (!login)
146
+ return input.readResponse();
147
+ return fake.exchanges() === 1 ? input.loginResponse("warm") : pending.promise.then(cloneResponse);
148
+ });
149
+ await input.create(fake.fetchImpl).read();
150
+ const checks = Array.from({ length: 3 }, () => input.create(fake.fetchImpl).login());
151
+ expect(fake.exchanges()).toBe(2);
152
+ pending.resolve(input.loginResponse("fresh"));
153
+ expect(await Promise.all(checks)).toEqual(["fresh", "fresh", "fresh"]);
154
+ });
155
+ test("aborting the initiating waiter leaves the shared refresh running", async () => {
156
+ const pending = Promise.withResolvers();
157
+ const fake = provider((login, init) => {
158
+ if (!login)
159
+ return input.readResponse();
160
+ expect(init.signal).toBeUndefined();
161
+ return pending.promise;
162
+ });
163
+ const controller = new AbortController;
164
+ const cancelled = input.create(fake.fetchImpl).login({ signal: controller.signal });
165
+ const survivor = input.create(fake.fetchImpl).read();
166
+ controller.abort(new Error("wait cancelled"));
167
+ await expect(cancelled).rejects.toThrow("wait cancelled");
168
+ pending.resolve(input.loginResponse("survived"));
169
+ await survivor;
170
+ await input.create(fake.fetchImpl).read();
171
+ expect(fake.exchanges()).toBe(1);
172
+ });
173
+ test("a pre-aborted waiter starts no exchange", async () => {
174
+ const fake = provider(() => {
175
+ throw new Error("unexpected fetch");
176
+ });
177
+ const controller = new AbortController;
178
+ controller.abort(new Error("already cancelled"));
179
+ await expect(input.create(fake.fetchImpl).login({ signal: controller.signal })).rejects.toThrow("already cancelled");
180
+ expect(fake.exchanges()).toBe(0);
181
+ });
182
+ test("a second 401 is surfaced without expanding the retry budget", async () => {
183
+ const fake = provider((login) => login ? input.loginResponse("rejected") : new Response("", { status: 401 }));
184
+ await expect(input.create(fake.fetchImpl).read()).rejects.toMatchObject({
185
+ status: 401
186
+ });
187
+ expect(fake.exchanges()).toBe(2);
188
+ expect(fake.usedTokens).toHaveLength(2);
189
+ });
190
+ });
191
+ }
192
+ // src/testing/helpers.ts
193
+ var noop = () => {};
194
+ var noopLogger = {
195
+ trace: noop,
196
+ debug: noop,
197
+ info: noop,
198
+ warn: noop,
199
+ error: noop,
200
+ fatal: noop,
201
+ bind: () => noopLogger,
202
+ child: () => noopLogger
203
+ };
204
+ function mockCallArg(fn, callIndex = 0, argIndex = 0) {
205
+ return fn.mock.calls[callIndex]?.[argIndex];
206
+ }
207
+ // src/testing/module-mocker.ts
208
+ class ModuleMocker {
209
+ resolve;
210
+ restorers = [];
211
+ constructor(resolve = (specifier) => specifier) {
212
+ this.resolve = resolve;
213
+ }
214
+ async mock(specifier, renderMocks) {
215
+ const { mock } = await import("bun:test");
216
+ const resolved = this.resolve(specifier);
217
+ const original = { ...await import(resolved) };
218
+ const merged = { ...original, ...renderMocks() };
219
+ mock.module(resolved, () => merged);
220
+ this.restorers.push(() => {
221
+ mock.module(resolved, () => original);
222
+ });
223
+ }
224
+ clear() {
225
+ for (const restore of this.restorers)
226
+ restore();
227
+ this.restorers = [];
228
+ }
229
+ }
230
+ // src/testing/prisma.ts
231
+ function createPrismaMock(createMock) {
232
+ const created = [];
233
+ const reset = () => {
234
+ for (const method of created)
235
+ method.mockReset();
236
+ };
237
+ const methodsOf = (methods, key) => {
238
+ let method = methods.get(key);
239
+ if (!method) {
240
+ method = createMock();
241
+ created.push(method);
242
+ methods.set(key, method);
243
+ }
244
+ return method;
245
+ };
246
+ const delegates = new Map;
247
+ const rootMethods = new Map;
248
+ const delegateFor = (model) => {
249
+ let delegate = delegates.get(model);
250
+ if (!delegate) {
251
+ const methods = new Map;
252
+ delegate = new Proxy({}, {
253
+ get: (_target, method) => typeof method === "symbol" ? undefined : methodsOf(methods, method)
254
+ });
255
+ delegates.set(model, delegate);
256
+ }
257
+ return delegate;
258
+ };
259
+ return new Proxy({}, {
260
+ get(_target, property) {
261
+ if (property === "$reset")
262
+ return reset;
263
+ if (typeof property === "symbol")
264
+ return;
265
+ if (property.startsWith("$"))
266
+ return methodsOf(rootMethods, property);
267
+ return delegateFor(property);
268
+ }
269
+ });
270
+ }
271
+ // src/testing/window.ts
272
+ function createTestStorage() {
273
+ const store = new Map;
274
+ return {
275
+ get length() {
276
+ return store.size;
277
+ },
278
+ clear: () => store.clear(),
279
+ getItem: (key) => store.get(key) ?? null,
280
+ key: (index) => [...store.keys()][index] ?? null,
281
+ removeItem: (key) => {
282
+ store.delete(key);
283
+ },
284
+ setItem: (key, value) => {
285
+ store.set(key, String(value));
286
+ }
287
+ };
288
+ }
289
+ var noop2 = () => {};
290
+ function locationOf(href) {
291
+ const { origin, protocol, host, hostname, port, pathname, search, hash } = new URL(href);
292
+ return {
293
+ href,
294
+ origin,
295
+ protocol,
296
+ host,
297
+ hostname,
298
+ port,
299
+ pathname,
300
+ search,
301
+ hash
302
+ };
303
+ }
304
+ function isObject(value) {
305
+ return typeof value === "object" && value !== null;
306
+ }
307
+ function installWindowShim(href = "http://localhost/") {
308
+ const shim = {
309
+ location: locationOf(href),
310
+ addEventListener: noop2,
311
+ removeEventListener: noop2,
312
+ localStorage: createTestStorage()
313
+ };
314
+ const assign = (value) => {
315
+ if (!isObject(value))
316
+ return;
317
+ if (value.localStorage)
318
+ shim.localStorage = value.localStorage;
319
+ shim.location = isObject(value.location) && typeof value.location.href === "string" ? value.location : locationOf(href);
320
+ shim.addEventListener = typeof value.addEventListener === "function" ? value.addEventListener : noop2;
321
+ shim.removeEventListener = typeof value.removeEventListener === "function" ? value.removeEventListener : noop2;
322
+ };
323
+ const install = () => {
324
+ Object.defineProperty(globalThis, "window", {
325
+ configurable: true,
326
+ enumerable: true,
327
+ get: () => shim,
328
+ set: assign
329
+ });
330
+ };
331
+ install();
332
+ return () => {
333
+ shim.location = locationOf(href);
334
+ shim.addEventListener = noop2;
335
+ shim.removeEventListener = noop2;
336
+ shim.localStorage = createTestStorage();
337
+ install();
338
+ };
339
+ }
340
+ export {
341
+ ModuleMocker,
342
+ connectionAuthContract,
343
+ createPrismaMock,
344
+ createTestStorage,
345
+ installWindowShim,
346
+ mockCallArg,
347
+ noopLogger
348
+ };