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.
- package/README.md +133 -7
- package/dist/api-wrapper/connection-auth.d.ts +61 -0
- package/dist/cli/fields.d.ts +30 -0
- package/dist/cli/index.d.ts +122 -0
- package/dist/format/index.d.ts +91 -0
- package/dist/format/phone.d.ts +19 -0
- package/dist/http/index.d.ts +14 -0
- package/dist/i18n/catalog.d.ts +47 -0
- package/dist/i18n/check.d.ts +45 -0
- package/dist/i18n/cli.d.ts +2 -0
- package/dist/i18n/content-rules.d.ts +55 -0
- package/dist/i18n/i18next.d.ts +39 -0
- package/dist/i18n/index.d.ts +16 -1
- package/dist/i18n/locales.d.ts +62 -0
- package/dist/i18n/vite.d.ts +16 -0
- package/dist/i18n/vue.d.ts +93 -0
- package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
- package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
- package/dist/index-5n2e326q.js +409 -0
- package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
- package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
- package/dist/index-6ragggbs.js +473 -0
- package/dist/index-mq2qgc20.js +116 -0
- package/dist/index-najbyzpr.js +99 -0
- package/dist/index-ww09bm49.js +250 -0
- package/dist/iso/countries.d.ts +262 -0
- package/dist/iso/currencies.d.ts +165 -0
- package/dist/iso/index.d.ts +4 -0
- package/dist/iso/languages.d.ts +195 -0
- package/dist/iso/phone-language.d.ts +5 -0
- package/dist/promise/index.d.ts +31 -0
- package/dist/redis/client.d.ts +49 -0
- package/dist/redis/concurrency-limit.d.ts +63 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/lua-script.d.ts +13 -0
- package/dist/secrets/infisical-identity-file.d.ts +12 -0
- package/dist/src/api-wrapper/connection-auth.js +82 -0
- package/dist/src/api-wrapper/index.js +1 -1
- package/dist/src/cli/index.js +10 -0
- package/dist/src/format/index.js +79 -2
- package/dist/src/format/phone.js +21 -0
- package/dist/src/http/index.js +69 -0
- package/dist/src/i18n/check.js +15 -0
- package/dist/src/i18n/cli.js +71 -0
- package/dist/src/i18n/i18next.js +48 -0
- package/dist/src/i18n/index.js +16 -34
- package/dist/src/i18n/vite.js +25 -0
- package/dist/src/i18n/vue.js +51 -0
- package/dist/src/index.js +22 -6
- package/dist/src/iso/index.js +655 -0
- package/dist/src/redis/index.js +577 -0
- package/dist/src/secrets/index.js +78 -35
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/storage/index.js +8 -117
- package/dist/src/testing/index.js +348 -0
- package/dist/src/zod/index.js +20 -253
- package/dist/string/index.d.ts +21 -0
- package/dist/testing/connection-auth-contract.d.ts +43 -0
- package/dist/testing/helpers.d.ts +26 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/module-mocker.d.ts +30 -0
- package/dist/testing/prisma.d.ts +30 -0
- package/dist/testing/window.d.ts +18 -0
- package/dist/typedswitch/index.d.ts +55 -34
- package/dist/zod/index.d.ts +10 -0
- package/dist/zod/introspect.d.ts +2 -1
- package/package.json +97 -2
- package/src/api-wrapper/README.md +57 -2
- package/src/api-wrapper/SKILL.md +256 -0
- package/src/api-wrapper/connection-auth.ts +142 -0
- package/src/cli/README.md +220 -0
- package/src/cli/fields.ts +213 -0
- package/src/cli/index.ts +532 -0
- package/src/format/README.md +82 -2
- package/src/format/index.ts +148 -0
- package/src/format/phone.ts +43 -0
- package/src/http/README.md +25 -0
- package/src/http/index.ts +94 -0
- package/src/i18n/README.md +206 -3
- package/src/i18n/catalog.ts +139 -0
- package/src/i18n/check.ts +376 -0
- package/src/i18n/cli.ts +90 -0
- package/src/i18n/content-rules.ts +179 -0
- package/src/i18n/i18next.ts +89 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/locales.ts +135 -0
- package/src/i18n/vite.ts +40 -0
- package/src/i18n/vue.ts +82 -0
- package/src/index.ts +1 -1
- package/src/iso/README.md +44 -0
- package/src/iso/countries.ts +272 -0
- package/src/iso/currencies.ts +172 -0
- package/src/iso/index.ts +8 -0
- package/src/iso/languages.ts +208 -0
- package/src/iso/phone-language.ts +46 -0
- package/src/promise/index.ts +100 -0
- package/src/redis/README.md +112 -0
- package/src/redis/client.ts +239 -0
- package/src/redis/concurrency-limit.ts +591 -0
- package/src/redis/index.ts +15 -0
- package/src/redis/lua-script.ts +43 -0
- package/src/secrets/README.md +5 -1
- package/src/secrets/infisical-identity-file.ts +69 -0
- package/src/secrets/infisical.ts +18 -3
- package/src/string/index.ts +59 -0
- package/src/testing/README.md +119 -0
- package/src/testing/connection-auth-contract.ts +303 -0
- package/src/testing/helpers.ts +41 -0
- package/src/testing/index.ts +12 -0
- package/src/testing/module-mocker.ts +50 -0
- package/src/testing/prisma.ts +78 -0
- package/src/testing/window.ts +119 -0
- package/src/typedswitch/README.md +55 -39
- package/src/typedswitch/index.ts +97 -83
- package/src/zod/index.ts +20 -0
- package/src/zod/introspect.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HttpResponseError2,
|
|
3
3
|
createApiClient2
|
|
4
|
-
} from "../../index-
|
|
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
|
|
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-
|
|
105
|
+
// src/secrets/infisical-identity-file.ts
|
|
106
106
|
import { z } from "zod";
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 =
|
|
134
|
-
var infisicalSecretSchema =
|
|
135
|
-
secretKey:
|
|
136
|
-
secretValue:
|
|
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 =
|
|
139
|
-
secrets:
|
|
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:
|
|
260
|
-
Authorization:
|
|
261
|
-
"User-Agent":
|
|
302
|
+
reqHeadersSchema: z3.object({
|
|
303
|
+
Authorization: z3.string(),
|
|
304
|
+
"User-Agent": z3.string()
|
|
262
305
|
}),
|
|
263
|
-
reqBodySchema:
|
|
264
|
-
resSchema:
|
|
265
|
-
token:
|
|
266
|
-
isMfaEnabled:
|
|
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:
|
|
274
|
-
clientId:
|
|
275
|
-
clientSecret:
|
|
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:
|
|
284
|
-
Authorization:
|
|
326
|
+
reqHeadersSchema: z3.object({
|
|
327
|
+
Authorization: z3.string()
|
|
285
328
|
}),
|
|
286
|
-
reqQuerySchema:
|
|
287
|
-
projectId:
|
|
288
|
-
environment:
|
|
289
|
-
secretPath:
|
|
290
|
-
viewSecretValue:
|
|
291
|
-
expandSecretReferences:
|
|
292
|
-
includeImports:
|
|
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
|
}
|
|
@@ -1,119 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
};
|