@hasna/contacts 0.7.0 → 0.9.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 +93 -13
- package/dist/browser/cli.d.ts +4 -0
- package/dist/browser/cli.d.ts.map +1 -0
- package/dist/browser/identity.d.ts +3 -0
- package/dist/browser/identity.d.ts.map +1 -0
- package/dist/browser/install.d.ts +32 -0
- package/dist/browser/install.d.ts.map +1 -0
- package/dist/browser/native.d.ts +3 -0
- package/dist/browser/native.d.ts.map +1 -0
- package/dist/browser/native.js +822 -0
- package/dist/browser/protocol.d.ts +26 -0
- package/dist/browser/protocol.d.ts.map +1 -0
- package/dist/browser/values.d.ts +18 -0
- package/dist/browser/values.d.ts.map +1 -0
- package/dist/cli/commands/core.d.ts.map +1 -1
- package/dist/cli/index.js +479 -149
- package/dist/cli/status-domain.preload.d.ts +1 -1
- package/dist/cli/status-domain.preload.d.ts.map +1 -1
- package/dist/cli/status-fixture.d.ts +17 -0
- package/dist/cli/status-fixture.d.ts.map +1 -0
- package/dist/cloud/http-storage.d.ts +20 -12
- package/dist/cloud/http-storage.d.ts.map +1 -1
- package/dist/cloud/resolver-inputs.d.ts +51 -0
- package/dist/cloud/resolver-inputs.d.ts.map +1 -0
- package/dist/db/paths.d.ts +4 -4
- package/dist/db/paths.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +19 -0
- package/dist/generated/storage-kit/backend.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +2 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/own.d.ts +11 -0
- package/dist/generated/storage-kit/own.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +5 -17
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/query.d.ts +1 -1
- package/dist/generated/storage-kit/query.d.ts.map +1 -1
- package/dist/generated/storage-kit/tls.d.ts +30 -3
- package/dist/generated/storage-kit/tls.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -28
- package/dist/lib/audience-contract.d.ts +9 -9
- package/dist/lib/compact-output.d.ts +67 -0
- package/dist/lib/compact-output.d.ts.map +1 -0
- package/dist/mcp/handlers/core.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +11 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +490 -212
- package/dist/mcp/profile.d.ts +8 -0
- package/dist/mcp/profile.d.ts.map +1 -0
- package/dist/mcp/register-tools.d.ts +3 -1
- package/dist/mcp/register-tools.d.ts.map +1 -1
- package/dist/mcp/startup-gate.d.ts +48 -0
- package/dist/mcp/startup-gate.d.ts.map +1 -0
- package/dist/mcp/storage-tools.d.ts +2 -1
- package/dist/mcp/storage-tools.d.ts.map +1 -1
- package/dist/mcp/tools.d.ts +7 -45
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +70 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +990 -7
- package/dist/server/index.js +231 -59
- package/dist/server/pg-store.d.ts +1 -0
- package/dist/server/pg-store.d.ts.map +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/docs/chrome-autofill.md +45 -0
- package/extension/background.js +284 -0
- package/extension/content.js +387 -0
- package/extension/detect.js +228 -0
- package/extension/fill.js +112 -0
- package/extension/icons/128.png +0 -0
- package/extension/icons/16.png +0 -0
- package/extension/icons/32.png +0 -0
- package/extension/icons/48.png +0 -0
- package/extension/icons/icon.svg +17 -0
- package/extension/manifest.json +54 -0
- package/extension/popup.css +78 -0
- package/extension/popup.html +47 -0
- package/extension/popup.js +166 -0
- package/hasna.contract.json +20 -6
- package/package.json +10 -6
- package/dist/lib/config.d.ts +0 -7
- package/dist/lib/config.d.ts.map +0 -1
package/dist/sdk/index.js
CHANGED
|
@@ -1,4 +1,937 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// node_modules/@hasna/contracts/dist/client/transport.js
|
|
3
|
+
import { isIP } from "net";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { closeSync, fstatSync, openSync, readFileSync } from "fs";
|
|
6
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { hostname as osHostname } from "os";
|
|
9
|
+
import { isAbsolute, join } from "path";
|
|
10
|
+
function envToken(name) {
|
|
11
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
12
|
+
}
|
|
13
|
+
function clientTransportEnvKeys(name) {
|
|
14
|
+
const envSegment = envToken(name);
|
|
15
|
+
return {
|
|
16
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
17
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function credentialOverrideEnvKey(name) {
|
|
21
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
22
|
+
}
|
|
23
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
24
|
+
function credentialPointerEnvKey(name) {
|
|
25
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class CredentialResolutionError extends Error {
|
|
29
|
+
appName;
|
|
30
|
+
attempted;
|
|
31
|
+
constructor(appName, message, attempted) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "CredentialResolutionError";
|
|
34
|
+
this.appName = appName;
|
|
35
|
+
this.attempted = attempted;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class CredentialFileUnsafeError extends Error {
|
|
40
|
+
path;
|
|
41
|
+
constructor(path, reason) {
|
|
42
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
43
|
+
this.name = "CredentialFileUnsafeError";
|
|
44
|
+
this.path = path;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
48
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
49
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
50
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
51
|
+
var CONFIG_SUBDIR = "config";
|
|
52
|
+
var CREDENTIALS_FILE = "credentials";
|
|
53
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
54
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
55
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
56
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
57
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
58
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
59
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
60
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
61
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
62
|
+
function homeDir(env) {
|
|
63
|
+
const home = env.HOME?.trim();
|
|
64
|
+
return home ? home : null;
|
|
65
|
+
}
|
|
66
|
+
function absoluteOverride(env, key) {
|
|
67
|
+
const value = env[key]?.trim();
|
|
68
|
+
return value && isAbsolute(value) ? value : null;
|
|
69
|
+
}
|
|
70
|
+
function hasnaHomeDir(env) {
|
|
71
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
72
|
+
if (override)
|
|
73
|
+
return override;
|
|
74
|
+
const home = homeDir(env);
|
|
75
|
+
return home ? join(home, HASNA_HOME_DIR) : null;
|
|
76
|
+
}
|
|
77
|
+
function appConfigDir(name, env) {
|
|
78
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
79
|
+
if (configRoot)
|
|
80
|
+
return join(configRoot, name);
|
|
81
|
+
const root = hasnaHomeDir(env);
|
|
82
|
+
return root ? join(root, name, CONFIG_SUBDIR) : null;
|
|
83
|
+
}
|
|
84
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
85
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
86
|
+
return [];
|
|
87
|
+
const directory = appConfigDir(name, env);
|
|
88
|
+
if (!directory)
|
|
89
|
+
return [];
|
|
90
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
91
|
+
return [{ path: join(directory, file), tier: "disk" }];
|
|
92
|
+
}
|
|
93
|
+
function credentialDiskSources(name, env) {
|
|
94
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
95
|
+
}
|
|
96
|
+
function profileDiskSources(name, env, profile) {
|
|
97
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
98
|
+
}
|
|
99
|
+
function parseEnvFile(text) {
|
|
100
|
+
const values = new Map;
|
|
101
|
+
const unusable = new Set;
|
|
102
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
103
|
+
const line = rawLine.trim();
|
|
104
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
105
|
+
continue;
|
|
106
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
107
|
+
const equals = withoutExport.indexOf("=");
|
|
108
|
+
if (equals <= 0)
|
|
109
|
+
continue;
|
|
110
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
111
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
112
|
+
continue;
|
|
113
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
114
|
+
const quote = value[0];
|
|
115
|
+
if (quote === '"' || quote === "'") {
|
|
116
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
117
|
+
unusable.add(key);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
value = value.slice(1, -1);
|
|
121
|
+
}
|
|
122
|
+
if (value.trim().length === 0) {
|
|
123
|
+
unusable.add(key);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (values.has(key) && values.get(key) !== value)
|
|
127
|
+
unusable.add(key);
|
|
128
|
+
values.set(key, value);
|
|
129
|
+
}
|
|
130
|
+
return { values, unusable };
|
|
131
|
+
}
|
|
132
|
+
function configFileModeAllowed(mode) {
|
|
133
|
+
const permissions = mode & 4095;
|
|
134
|
+
return permissions === 256 || permissions === 384;
|
|
135
|
+
}
|
|
136
|
+
function configFileReadsCoherent(before, after) {
|
|
137
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
138
|
+
}
|
|
139
|
+
function readAppConfigFile(path) {
|
|
140
|
+
const unsafe = (reason) => {
|
|
141
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
142
|
+
};
|
|
143
|
+
let fd = -1;
|
|
144
|
+
try {
|
|
145
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const code = error.code;
|
|
148
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
149
|
+
return null;
|
|
150
|
+
if (code === "ELOOP")
|
|
151
|
+
unsafe("the path is a symlink");
|
|
152
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const before = fstatSync(fd);
|
|
156
|
+
if (!before.isFile())
|
|
157
|
+
unsafe("the path is not a regular file");
|
|
158
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
159
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
160
|
+
}
|
|
161
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
162
|
+
if (uid !== undefined && before.uid !== uid)
|
|
163
|
+
unsafe("the file is not owned by the current user");
|
|
164
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
165
|
+
unsafe("the file exceeds the size limit");
|
|
166
|
+
const bytes = readFileSync(fd);
|
|
167
|
+
const after = fstatSync(fd);
|
|
168
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
169
|
+
unsafe("the file changed while being read");
|
|
170
|
+
}
|
|
171
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
172
|
+
} finally {
|
|
173
|
+
if (fd !== -1)
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
178
|
+
const parsed = readAppConfigFile(path);
|
|
179
|
+
if (!parsed)
|
|
180
|
+
return null;
|
|
181
|
+
for (const key of apiKeyKeys) {
|
|
182
|
+
if (parsed.unusable.has(key)) {
|
|
183
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
187
|
+
if (new Set(values).size > 1) {
|
|
188
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
189
|
+
}
|
|
190
|
+
return values[0] ?? null;
|
|
191
|
+
}
|
|
192
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
193
|
+
function appConfigDiskValue(name, env, keys) {
|
|
194
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
195
|
+
if (wanted.length === 0)
|
|
196
|
+
return null;
|
|
197
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
198
|
+
const parsed = readAppConfigFile(path);
|
|
199
|
+
if (!parsed)
|
|
200
|
+
continue;
|
|
201
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
202
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
203
|
+
}
|
|
204
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
205
|
+
if (new Set(values).size > 1)
|
|
206
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
207
|
+
for (const key of wanted) {
|
|
208
|
+
if (parsed.unusable.has(key))
|
|
209
|
+
return { key, value: "", path, unusable: true };
|
|
210
|
+
const value = parsed.values.get(key)?.trim();
|
|
211
|
+
if (value)
|
|
212
|
+
return { key, value, path };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
function assertUsableCredential(appName, source, value) {
|
|
218
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
219
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
220
|
+
}
|
|
221
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
222
|
+
return;
|
|
223
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
224
|
+
}
|
|
225
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
226
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
227
|
+
function sealCredential(fields) {
|
|
228
|
+
const { apiKey } = fields;
|
|
229
|
+
const visible = {
|
|
230
|
+
tier: fields.tier,
|
|
231
|
+
source: fields.source,
|
|
232
|
+
deliberate: fields.deliberate,
|
|
233
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
234
|
+
warning: fields.warning
|
|
235
|
+
};
|
|
236
|
+
const sealed = { ...visible };
|
|
237
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
238
|
+
value: apiKey,
|
|
239
|
+
enumerable: false,
|
|
240
|
+
writable: false,
|
|
241
|
+
configurable: false
|
|
242
|
+
});
|
|
243
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
244
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
245
|
+
value: fields.pointerVaultKey,
|
|
246
|
+
enumerable: false,
|
|
247
|
+
writable: false,
|
|
248
|
+
configurable: false
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
252
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
253
|
+
enumerable: false,
|
|
254
|
+
writable: false,
|
|
255
|
+
configurable: false
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
258
|
+
value: true,
|
|
259
|
+
enumerable: false,
|
|
260
|
+
writable: false,
|
|
261
|
+
configurable: false
|
|
262
|
+
});
|
|
263
|
+
return Object.freeze(sealed);
|
|
264
|
+
}
|
|
265
|
+
function firstEnvValue(env, keys) {
|
|
266
|
+
for (const key of keys) {
|
|
267
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
268
|
+
continue;
|
|
269
|
+
const value = env[key]?.trim();
|
|
270
|
+
if (value)
|
|
271
|
+
return { key, value };
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
276
|
+
function isAmbientEnvironment(env) {
|
|
277
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
278
|
+
}
|
|
279
|
+
function defaultKeychainRunner(argv) {
|
|
280
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
281
|
+
encoding: "utf8",
|
|
282
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
283
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
status: result.status,
|
|
287
|
+
stdout: result.stdout ?? "",
|
|
288
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function keychainTierEnabled(env, options) {
|
|
292
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
293
|
+
return false;
|
|
294
|
+
if (options.enabled !== undefined)
|
|
295
|
+
return options.enabled;
|
|
296
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
297
|
+
}
|
|
298
|
+
function keychainAccount(env, options) {
|
|
299
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
300
|
+
if (station)
|
|
301
|
+
return station;
|
|
302
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
303
|
+
if (host)
|
|
304
|
+
return host;
|
|
305
|
+
const user = env.USER?.trim();
|
|
306
|
+
return user || null;
|
|
307
|
+
}
|
|
308
|
+
function keychainFailureHint(text) {
|
|
309
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
310
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
311
|
+
return clean ? `: ${clean}` : "";
|
|
312
|
+
}
|
|
313
|
+
function readKeychainItem(name, env, kind, options) {
|
|
314
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
315
|
+
return null;
|
|
316
|
+
const account = keychainAccount(env, options);
|
|
317
|
+
if (!account)
|
|
318
|
+
return null;
|
|
319
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
320
|
+
const source = `keychain:${service}@${account}`;
|
|
321
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
327
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
328
|
+
}
|
|
329
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
330
|
+
return null;
|
|
331
|
+
if (result.status !== 0) {
|
|
332
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
333
|
+
}
|
|
334
|
+
const value = result.stdout.trim();
|
|
335
|
+
if (!value) {
|
|
336
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
337
|
+
}
|
|
338
|
+
return { value, source };
|
|
339
|
+
}
|
|
340
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
341
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
342
|
+
}
|
|
343
|
+
function snapshotClientEnvironment(name, env) {
|
|
344
|
+
const keys = clientTransportEnvKeys(name);
|
|
345
|
+
const ambient = isAmbientEnvironment(env);
|
|
346
|
+
const snapshot = Object.create(null);
|
|
347
|
+
for (const key of [
|
|
348
|
+
...keys.apiUrlKeys,
|
|
349
|
+
...keys.apiKeyKeys,
|
|
350
|
+
credentialOverrideEnvKey(name),
|
|
351
|
+
credentialPointerEnvKey(name),
|
|
352
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
353
|
+
"HOME",
|
|
354
|
+
HASNA_HOME_ENV_KEY,
|
|
355
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
356
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
357
|
+
"USER"
|
|
358
|
+
]) {
|
|
359
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
360
|
+
if (!descriptor)
|
|
361
|
+
continue;
|
|
362
|
+
if (!("value" in descriptor)) {
|
|
363
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
364
|
+
}
|
|
365
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
366
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
367
|
+
}
|
|
368
|
+
snapshot[key] = descriptor.value;
|
|
369
|
+
}
|
|
370
|
+
if (ambient) {
|
|
371
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
372
|
+
value: true,
|
|
373
|
+
enumerable: false,
|
|
374
|
+
writable: false,
|
|
375
|
+
configurable: false
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return Object.freeze(snapshot);
|
|
379
|
+
}
|
|
380
|
+
function resolveCredential(name, env, options = {}) {
|
|
381
|
+
env = snapshotClientEnvironment(name, env);
|
|
382
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
383
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
384
|
+
if (options.apiKey !== undefined) {
|
|
385
|
+
const explicitKey = options.apiKey.trim();
|
|
386
|
+
if (!explicitKey) {
|
|
387
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
388
|
+
}
|
|
389
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
390
|
+
return sealCredential({
|
|
391
|
+
apiKey: explicitKey,
|
|
392
|
+
tier: "argument",
|
|
393
|
+
source: "explicit apiKey argument",
|
|
394
|
+
deliberate: true,
|
|
395
|
+
diskCandidates: diskPaths,
|
|
396
|
+
warning: null
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
400
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
401
|
+
if (overrideRaw !== undefined) {
|
|
402
|
+
const override = overrideRaw.trim();
|
|
403
|
+
if (!override) {
|
|
404
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
405
|
+
}
|
|
406
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
407
|
+
return sealCredential({
|
|
408
|
+
apiKey: override,
|
|
409
|
+
tier: "override",
|
|
410
|
+
source: overrideKeyName,
|
|
411
|
+
deliberate: true,
|
|
412
|
+
diskCandidates: diskPaths,
|
|
413
|
+
warning: null
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
417
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
418
|
+
if (pointerRaw !== undefined) {
|
|
419
|
+
const pointer = pointerRaw.trim();
|
|
420
|
+
if (!pointer) {
|
|
421
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
422
|
+
}
|
|
423
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
424
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
425
|
+
}
|
|
426
|
+
return sealCredential({
|
|
427
|
+
apiKey: "",
|
|
428
|
+
pointerVaultKey: pointer,
|
|
429
|
+
tier: "pointer",
|
|
430
|
+
source: pointerKeyName,
|
|
431
|
+
deliberate: true,
|
|
432
|
+
diskCandidates: diskPaths,
|
|
433
|
+
warning: null
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
437
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
438
|
+
}
|
|
439
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
440
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
441
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
442
|
+
}
|
|
443
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
444
|
+
if (profile) {
|
|
445
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
446
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
447
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
448
|
+
}
|
|
449
|
+
const paths = profileDiskSources(name, env, profile);
|
|
450
|
+
for (const path of paths) {
|
|
451
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
452
|
+
if (value) {
|
|
453
|
+
assertUsableCredential(name, path, value);
|
|
454
|
+
return sealCredential({
|
|
455
|
+
apiKey: value,
|
|
456
|
+
tier: "profile",
|
|
457
|
+
source: path,
|
|
458
|
+
deliberate: true,
|
|
459
|
+
diskCandidates: paths,
|
|
460
|
+
warning: null
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
465
|
+
}
|
|
466
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
467
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
468
|
+
if (blankEnv) {
|
|
469
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
470
|
+
}
|
|
471
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
472
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
473
|
+
}
|
|
474
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
475
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
476
|
+
if (keychainHit) {
|
|
477
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
478
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
|
|
479
|
+
return sealCredential({
|
|
480
|
+
apiKey: keychainHit.value,
|
|
481
|
+
tier: "keychain",
|
|
482
|
+
source: keychainHit.source,
|
|
483
|
+
deliberate: false,
|
|
484
|
+
diskCandidates: diskPaths,
|
|
485
|
+
warning
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
489
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
490
|
+
if (diskHits.length > 0) {
|
|
491
|
+
const winner = diskHits[0];
|
|
492
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
493
|
+
const divergentSources = [
|
|
494
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
495
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
496
|
+
];
|
|
497
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
498
|
+
return sealCredential({
|
|
499
|
+
apiKey: winner.value,
|
|
500
|
+
tier: winner.src.tier,
|
|
501
|
+
source: winner.src.path,
|
|
502
|
+
deliberate: false,
|
|
503
|
+
diskCandidates: diskPaths,
|
|
504
|
+
warning
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (envHit) {
|
|
508
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
509
|
+
return sealCredential({
|
|
510
|
+
apiKey: envHit.value,
|
|
511
|
+
tier: "env",
|
|
512
|
+
source: envHit.key,
|
|
513
|
+
deliberate: false,
|
|
514
|
+
diskCandidates: diskPaths,
|
|
515
|
+
warning: null
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
521
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
522
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
523
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
524
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
525
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
526
|
+
}
|
|
527
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
528
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
529
|
+
function isValidDnsDomain(value) {
|
|
530
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
534
|
+
}
|
|
535
|
+
function validateAppSlug(name) {
|
|
536
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
537
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
538
|
+
}
|
|
539
|
+
return name;
|
|
540
|
+
}
|
|
541
|
+
function rawAuthority(value) {
|
|
542
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
543
|
+
if (!match)
|
|
544
|
+
throw new Error("API URL must be absolute.");
|
|
545
|
+
const afterScheme = value.slice(match[0].length);
|
|
546
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
547
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
548
|
+
if (!authority)
|
|
549
|
+
throw new Error("API URL must include a hostname.");
|
|
550
|
+
return authority;
|
|
551
|
+
}
|
|
552
|
+
function assertCanonicalPort(port) {
|
|
553
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
554
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
555
|
+
}
|
|
556
|
+
const numericPort = Number(port);
|
|
557
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
558
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function canonicalAuthorityHostname(authority) {
|
|
562
|
+
let rawHostname;
|
|
563
|
+
if (authority.startsWith("[")) {
|
|
564
|
+
const closingBracket = authority.indexOf("]");
|
|
565
|
+
if (closingBracket === -1) {
|
|
566
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
567
|
+
}
|
|
568
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
569
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
570
|
+
if (portSuffix) {
|
|
571
|
+
if (!portSuffix.startsWith(":")) {
|
|
572
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
573
|
+
}
|
|
574
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
575
|
+
}
|
|
576
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
577
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
const firstColon = authority.indexOf(":");
|
|
581
|
+
const lastColon = authority.lastIndexOf(":");
|
|
582
|
+
if (firstColon !== lastColon) {
|
|
583
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
584
|
+
}
|
|
585
|
+
if (lastColon !== -1) {
|
|
586
|
+
const port = authority.slice(lastColon + 1);
|
|
587
|
+
assertCanonicalPort(port);
|
|
588
|
+
rawHostname = authority.slice(0, lastColon);
|
|
589
|
+
} else {
|
|
590
|
+
rawHostname = authority;
|
|
591
|
+
}
|
|
592
|
+
const ipVersion = isIP(rawHostname);
|
|
593
|
+
const numericAddressParts = rawHostname.split(".");
|
|
594
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
595
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
596
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return rawHostname.toLowerCase();
|
|
600
|
+
}
|
|
601
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
602
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
603
|
+
}
|
|
604
|
+
function toV1BaseUrl(apiUrl) {
|
|
605
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
606
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
607
|
+
}
|
|
608
|
+
const input = apiUrl.trim();
|
|
609
|
+
const authority = rawAuthority(input);
|
|
610
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
611
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
612
|
+
}
|
|
613
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
614
|
+
const url = new URL(input);
|
|
615
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
616
|
+
throw new Error("API URL must use http or https.");
|
|
617
|
+
}
|
|
618
|
+
if (url.username || url.password) {
|
|
619
|
+
throw new Error("API URL must not include credentials.");
|
|
620
|
+
}
|
|
621
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
622
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
623
|
+
}
|
|
624
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
625
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
626
|
+
}
|
|
627
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
628
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
629
|
+
}
|
|
630
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
631
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
632
|
+
}
|
|
633
|
+
if (url.search || url.hash) {
|
|
634
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
635
|
+
}
|
|
636
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
637
|
+
if (path.endsWith("/v1"))
|
|
638
|
+
path = path.slice(0, -"/v1".length);
|
|
639
|
+
url.pathname = `${path}/v1`;
|
|
640
|
+
return url.toString().replace(/\/+$/, "");
|
|
641
|
+
}
|
|
642
|
+
class ClientTransportConfigurationError extends Error {
|
|
643
|
+
appName;
|
|
644
|
+
sources;
|
|
645
|
+
constructor(appName, message, sources = []) {
|
|
646
|
+
super(message);
|
|
647
|
+
this.name = "ClientTransportConfigurationError";
|
|
648
|
+
this.appName = appName;
|
|
649
|
+
this.sources = Object.freeze([...sources]);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
653
|
+
env = snapshotClientEnvironment(name, env);
|
|
654
|
+
const keys = clientTransportEnvKeys(name);
|
|
655
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
656
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
657
|
+
if (blankUrl) {
|
|
658
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
659
|
+
}
|
|
660
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
661
|
+
if (controlledUrl) {
|
|
662
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
663
|
+
}
|
|
664
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
665
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
666
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
667
|
+
}
|
|
668
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
669
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
670
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
671
|
+
if (diskConfigUrlHit?.unusable) {
|
|
672
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
673
|
+
}
|
|
674
|
+
const urlCandidates = [
|
|
675
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
676
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
677
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
678
|
+
];
|
|
679
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
680
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
681
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
682
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
683
|
+
}
|
|
684
|
+
const warnings = [];
|
|
685
|
+
if (configuredUrl && !envUrlHit) {
|
|
686
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
687
|
+
}
|
|
688
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
689
|
+
if (!credential) {
|
|
690
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
691
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
692
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
693
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
694
|
+
}
|
|
695
|
+
if (credential.warning)
|
|
696
|
+
warnings.push(credential.warning);
|
|
697
|
+
let urlHit;
|
|
698
|
+
if (configuredUrl) {
|
|
699
|
+
urlHit = configuredUrl;
|
|
700
|
+
} else {
|
|
701
|
+
try {
|
|
702
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
705
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const apiUrlSource = urlHit.key;
|
|
709
|
+
let baseUrl;
|
|
710
|
+
try {
|
|
711
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
714
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
resolution: {
|
|
718
|
+
transport: "http",
|
|
719
|
+
transportSource: urlHit.key,
|
|
720
|
+
baseUrl,
|
|
721
|
+
apiUrlSource,
|
|
722
|
+
apiKeyPresent: true,
|
|
723
|
+
apiKeySource: credential.source,
|
|
724
|
+
apiKeyTier: credential.tier,
|
|
725
|
+
misconfigured: false,
|
|
726
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
727
|
+
},
|
|
728
|
+
credential
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
732
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
733
|
+
}
|
|
734
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
735
|
+
const paths = credentialDiskSources(name, env);
|
|
736
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
737
|
+
}
|
|
738
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
739
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
740
|
+
"host",
|
|
741
|
+
":authority",
|
|
742
|
+
"forwarded",
|
|
743
|
+
"x-forwarded-host",
|
|
744
|
+
"x-original-host"
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
// src/cloud/client-config.ts
|
|
748
|
+
import { createHash } from "crypto";
|
|
749
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
750
|
+
function invalid() {
|
|
751
|
+
throw new Error("CONTACTS_CLIENT_CONFIG_INVALID: blank, conflicting, or unstable client configuration; no request was sent.");
|
|
752
|
+
}
|
|
753
|
+
function checkAliases(values) {
|
|
754
|
+
const keys = clientTransportEnvKeys("contacts");
|
|
755
|
+
for (const group of [keys.apiUrlKeys, keys.apiKeyKeys]) {
|
|
756
|
+
const defined = group.filter((key) => values[key] !== undefined);
|
|
757
|
+
const normalized = defined.map((key) => {
|
|
758
|
+
const value = values[key].trim();
|
|
759
|
+
if (!value)
|
|
760
|
+
invalid();
|
|
761
|
+
if (group === keys.apiUrlKeys) {
|
|
762
|
+
try {
|
|
763
|
+
return toV1BaseUrl(value);
|
|
764
|
+
} catch {
|
|
765
|
+
invalid();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
});
|
|
770
|
+
if (new Set(normalized).size > 1)
|
|
771
|
+
invalid();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function checkDiskAliases(text) {
|
|
775
|
+
const values = {};
|
|
776
|
+
const wanted = new Set(Object.values(clientTransportEnvKeys("contacts")).flat());
|
|
777
|
+
for (const line of text.split(/\r?\n/)) {
|
|
778
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
|
|
779
|
+
if (!match || !wanted.has(match[1]))
|
|
780
|
+
continue;
|
|
781
|
+
let value = match[2].trim();
|
|
782
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
783
|
+
if (value.length < 2 || value.at(-1) !== value[0])
|
|
784
|
+
invalid();
|
|
785
|
+
value = value.slice(1, -1);
|
|
786
|
+
}
|
|
787
|
+
if (values[match[1]] !== undefined && values[match[1]] !== value)
|
|
788
|
+
invalid();
|
|
789
|
+
values[match[1]] = value;
|
|
790
|
+
}
|
|
791
|
+
checkAliases(values);
|
|
792
|
+
}
|
|
793
|
+
function clientConfigurationStamp(env) {
|
|
794
|
+
checkAliases(env);
|
|
795
|
+
const digest = createHash("sha256");
|
|
796
|
+
digest.update(JSON.stringify(Object.entries(env).sort(([a], [b]) => a.localeCompare(b))));
|
|
797
|
+
const profile = env.HASNA_PROFILE?.trim();
|
|
798
|
+
if (profile && !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(profile))
|
|
799
|
+
invalid();
|
|
800
|
+
const paths = [
|
|
801
|
+
...credentialDiskSourceList("contacts", env),
|
|
802
|
+
...profile ? credentialDiskSourceList("contacts", env, profile) : []
|
|
803
|
+
];
|
|
804
|
+
for (const { path } of paths) {
|
|
805
|
+
digest.update(path);
|
|
806
|
+
try {
|
|
807
|
+
const before = statSync(path, { bigint: true });
|
|
808
|
+
if (!before.isFile() || before.size > 65536n)
|
|
809
|
+
invalid();
|
|
810
|
+
const contents = readFileSync2(path);
|
|
811
|
+
const after = statSync(path, { bigint: true });
|
|
812
|
+
const identity = (s) => `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
|
|
813
|
+
if (identity(before) !== identity(after))
|
|
814
|
+
invalid();
|
|
815
|
+
checkDiskAliases(contents.toString("utf8"));
|
|
816
|
+
digest.update(identity(after)).update(contents);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error.code !== "ENOENT")
|
|
819
|
+
invalid();
|
|
820
|
+
digest.update("absent");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return digest.digest("hex");
|
|
824
|
+
}
|
|
825
|
+
function assertConfigurationUnchanged(env, expected) {
|
|
826
|
+
if (clientConfigurationStamp(env) !== expected)
|
|
827
|
+
invalid();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/cloud/resolver-inputs.ts
|
|
831
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
832
|
+
function isAmbientContactsEnv(env) {
|
|
833
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
834
|
+
return true;
|
|
835
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
836
|
+
}
|
|
837
|
+
function contactsResolverCredentials(env, credentials = {}) {
|
|
838
|
+
const keychain = { ...credentials.keychain };
|
|
839
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
840
|
+
keychain.enabled = isAmbientContactsEnv(env);
|
|
841
|
+
}
|
|
842
|
+
return { ...credentials, keychain };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/cloud/http-storage.ts
|
|
846
|
+
var RETIRED_CLIENT_SELECTOR_KEYS = [
|
|
847
|
+
"HASNA_CONTACTS_STORAGE_MODE",
|
|
848
|
+
"CONTACTS_STORAGE_MODE",
|
|
849
|
+
"HASNA_CONTACTS_MODE",
|
|
850
|
+
"CONTACTS_MODE",
|
|
851
|
+
"HASNA_CONTACTS_DB_PATH",
|
|
852
|
+
"CONTACTS_DB_PATH",
|
|
853
|
+
"HASNA_CONTACTS_DATABASE_URL",
|
|
854
|
+
"CONTACTS_DATABASE_URL"
|
|
855
|
+
];
|
|
856
|
+
function configuredKeys(env, keys) {
|
|
857
|
+
return keys.filter((key) => env[key] !== undefined && env[key].trim().length > 0);
|
|
858
|
+
}
|
|
859
|
+
function assertNoRetiredClientSelectors(env) {
|
|
860
|
+
const found = configuredKeys(env, RETIRED_CLIENT_SELECTOR_KEYS);
|
|
861
|
+
if (found.length === 0)
|
|
862
|
+
return;
|
|
863
|
+
throw new ContactsClientConfigurationError("RETIRED_CONTACTS_CLIENT_SELECTOR", `Contacts clients use only HASNA_CONTACTS_API_URL plus an API key resolved by @hasna/contracts. ` + `Remove retired client selector${found.length === 1 ? "" : "s"}: ${found.join(", ")}. ` + "PostgreSQL configuration belongs only on contacts-serve; local SQLite is available only through the explicit legacy migration command.");
|
|
864
|
+
}
|
|
865
|
+
function assertHttpsBaseUrl(baseUrl) {
|
|
866
|
+
const url = new URL(baseUrl);
|
|
867
|
+
if (url.protocol !== "https:") {
|
|
868
|
+
throw new ContactsClientConfigurationError("CONTACTS_API_HTTPS_REQUIRED", "HASNA_CONTACTS_API_URL must use HTTPS. Plain HTTP and local-store fallback are disabled for contacts clients.");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
class ContactsClientConfigurationError extends Error {
|
|
873
|
+
code;
|
|
874
|
+
constructor(code, message) {
|
|
875
|
+
super(`${code}: ${message}`);
|
|
876
|
+
this.code = code;
|
|
877
|
+
this.name = "ContactsClientConfigurationError";
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function unconfiguredResolution(issue, resolution, warning = null) {
|
|
881
|
+
return {
|
|
882
|
+
transport: "unconfigured",
|
|
883
|
+
baseUrl: null,
|
|
884
|
+
apiUrlSource: resolution?.apiUrlSource ?? null,
|
|
885
|
+
apiKeyPresent: resolution?.apiKeyPresent ?? false,
|
|
886
|
+
apiKeySource: resolution?.apiKeySource ?? null,
|
|
887
|
+
apiKeyTier: resolution?.apiKeyTier ?? null,
|
|
888
|
+
configured: false,
|
|
889
|
+
misconfigured: true,
|
|
890
|
+
issue,
|
|
891
|
+
warning
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function resolveContactsClientTransport(name, env = process.env, credentials = {}) {
|
|
895
|
+
if (name !== "contacts") {
|
|
896
|
+
throw new ContactsClientConfigurationError("CONTACTS_CLIENT_NAME_INVALID", "This resolver only accepts the contacts app slug.");
|
|
897
|
+
}
|
|
898
|
+
assertNoRetiredClientSelectors(env);
|
|
899
|
+
const stamp = clientConfigurationStamp(env);
|
|
900
|
+
let resolution;
|
|
901
|
+
try {
|
|
902
|
+
resolution = resolveClientTransport(name, env, {
|
|
903
|
+
credentials: contactsResolverCredentials(env, credentials)
|
|
904
|
+
});
|
|
905
|
+
} catch (error) {
|
|
906
|
+
assertConfigurationUnchanged(env, stamp);
|
|
907
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
908
|
+
return unconfiguredResolution(error.message, null);
|
|
909
|
+
}
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
assertConfigurationUnchanged(env, stamp);
|
|
913
|
+
try {
|
|
914
|
+
assertHttpsBaseUrl(resolution.baseUrl);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
return {
|
|
917
|
+
...unconfiguredResolution(error instanceof Error ? error.message : String(error), resolution),
|
|
918
|
+
apiKeyPresent: resolution.apiKeyPresent
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
transport: "https",
|
|
923
|
+
baseUrl: resolution.baseUrl,
|
|
924
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
925
|
+
apiKeyPresent: resolution.apiKeyPresent,
|
|
926
|
+
apiKeySource: resolution.apiKeySource,
|
|
927
|
+
apiKeyTier: resolution.apiKeyTier,
|
|
928
|
+
configured: true,
|
|
929
|
+
misconfigured: false,
|
|
930
|
+
issue: null,
|
|
931
|
+
warning: resolution.warning
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
2
935
|
// src/sdk/v1.generated.ts
|
|
3
936
|
class ApiError extends Error {
|
|
4
937
|
status;
|
|
@@ -259,15 +1192,11 @@ function validateBaseUrl(raw) {
|
|
|
259
1192
|
throw new Error("ContactsV1Client baseUrl must not contain credentials.");
|
|
260
1193
|
if (url.search || url.hash)
|
|
261
1194
|
throw new Error("ContactsV1Client baseUrl must not contain a query or fragment.");
|
|
262
|
-
|
|
263
|
-
if (path && path !== "/v1") {
|
|
264
|
-
throw new Error("ContactsV1Client baseUrl must be an authority root or end in /v1.");
|
|
265
|
-
}
|
|
266
|
-
url.pathname = "";
|
|
1195
|
+
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
267
1196
|
return url.toString().replace(/\/+$/, "");
|
|
268
1197
|
}
|
|
269
1198
|
function validateApiKey(apiKey) {
|
|
270
|
-
const key = apiKey.trim();
|
|
1199
|
+
const key = (apiKey ?? "").trim();
|
|
271
1200
|
if (!key)
|
|
272
1201
|
throw new Error("ContactsV1Client requires an API key.");
|
|
273
1202
|
if (/[^\t\x20-\x7e]/.test(key)) {
|
|
@@ -287,7 +1216,61 @@ class ContactsV1Client2 extends ContactsV1Client {
|
|
|
287
1216
|
});
|
|
288
1217
|
}
|
|
289
1218
|
}
|
|
1219
|
+
var CONTACTS_APP_NAME = "contacts";
|
|
1220
|
+
function contactsSdkAuthorityPinMessage() {
|
|
1221
|
+
return "an explicit baseUrl requires an explicit apiKey. The SDK never attaches a credential that " + "resolved for a different authority: pass `apiKey` explicitly, or omit `baseUrl` and let the " + "@hasna/contracts chain resolve both halves together.";
|
|
1222
|
+
}
|
|
1223
|
+
var SDK_HOSTED_ONLY = "The contacts SDK is hosted-only and never falls back to local data.";
|
|
1224
|
+
function resolveSdkCredential(env, chainOptions) {
|
|
1225
|
+
const credential = resolveCredential(CONTACTS_APP_NAME, env, chainOptions);
|
|
1226
|
+
if (!credential) {
|
|
1227
|
+
const diagnosis = resolveContactsClientTransport(CONTACTS_APP_NAME, env, chainOptions);
|
|
1228
|
+
throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", `${diagnosis.issue ?? "No contacts credential resolved."} ${SDK_HOSTED_ONLY}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (credential.tier === "pointer") {
|
|
1231
|
+
throw new ContactsClientConfigurationError("CONTACTS_CREDENTIAL_POINTER_UNSUPPORTED", `The contacts SDK resolves credentials synchronously and cannot complete the secrets-vault pointer ` + `${credential.source} per request. Use a literal tier instead: an explicit apiKey, the Keychain item ` + `hasna.credentials.${CONTACTS_APP_NAME}.api-key, ~/.hasna/${CONTACTS_APP_NAME}/config/credentials, ` + `or HASNA_CONTACTS_API_KEY.`);
|
|
1232
|
+
}
|
|
1233
|
+
return credential.apiKey;
|
|
1234
|
+
}
|
|
1235
|
+
function createContactsClient(options = {}) {
|
|
1236
|
+
const { baseUrl, apiKey, env, profile, keychain, ...clientOptions } = options;
|
|
1237
|
+
if (baseUrl !== undefined) {
|
|
1238
|
+
if (!apiKey)
|
|
1239
|
+
throw new ContactsClientConfigurationError("CONTACTS_CREDENTIAL_PINNED", contactsSdkAuthorityPinMessage());
|
|
1240
|
+
return new ContactsV1Client2({ ...clientOptions, baseUrl, apiKey });
|
|
1241
|
+
}
|
|
1242
|
+
const envObject = env ?? (typeof process !== "undefined" ? process.env : {});
|
|
1243
|
+
const requested = {
|
|
1244
|
+
...apiKey !== undefined ? { apiKey } : {},
|
|
1245
|
+
...profile !== undefined ? { profile } : {},
|
|
1246
|
+
...keychain !== undefined ? { keychain } : {}
|
|
1247
|
+
};
|
|
1248
|
+
const chainOptions = contactsResolverCredentials(envObject, requested);
|
|
1249
|
+
const initialKey = resolveSdkCredential(envObject, chainOptions);
|
|
1250
|
+
const resolution = resolveContactsClientTransport(CONTACTS_APP_NAME, envObject, { ...chainOptions, apiKey: initialKey });
|
|
1251
|
+
if (!resolution.configured || !resolution.baseUrl) {
|
|
1252
|
+
throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", `${resolution.issue ?? "The contacts API client is not configured."} ${SDK_HOSTED_ONLY}`);
|
|
1253
|
+
}
|
|
1254
|
+
const pinnedBaseUrl = resolution.baseUrl;
|
|
1255
|
+
const baseFetch = clientOptions.fetch ?? globalThis.fetch;
|
|
1256
|
+
const chainFetch = async (input, init) => {
|
|
1257
|
+
const snapshot = { ...envObject };
|
|
1258
|
+
const freshKey = resolveSdkCredential(snapshot, chainOptions);
|
|
1259
|
+
const current = resolveContactsClientTransport(CONTACTS_APP_NAME, snapshot, { ...chainOptions, apiKey: freshKey });
|
|
1260
|
+
if (!current.configured || current.baseUrl !== pinnedBaseUrl) {
|
|
1261
|
+
throw new ContactsClientConfigurationError("CONTACTS_AUTHORITY_CHANGED", "Client authority changed or disappeared; construct a new client before sending data.");
|
|
1262
|
+
}
|
|
1263
|
+
const headers = new Headers(init?.headers);
|
|
1264
|
+
headers.set("x-api-key", freshKey);
|
|
1265
|
+
return baseFetch(input, { ...init, headers });
|
|
1266
|
+
};
|
|
1267
|
+
return new ContactsV1Client2({ ...clientOptions, baseUrl: pinnedBaseUrl, apiKey: initialKey, fetch: chainFetch });
|
|
1268
|
+
}
|
|
290
1269
|
export {
|
|
1270
|
+
createContactsClient,
|
|
1271
|
+
contactsSdkAuthorityPinMessage,
|
|
291
1272
|
ContactsV1Client2 as ContactsV1Client,
|
|
292
|
-
ApiError as ContactsV1ApiError
|
|
1273
|
+
ApiError as ContactsV1ApiError,
|
|
1274
|
+
ContactsClientConfigurationError,
|
|
1275
|
+
CONTACTS_APP_NAME
|
|
293
1276
|
};
|