@hasna/domains 0.0.46 → 0.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 +58 -34
- package/dist/cli/commands/dns.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/index.js +4946 -2042
- package/dist/db/database.d.ts +6 -21
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/domains.d.ts +3 -14
- package/dist/db/domains.d.ts.map +1 -1
- package/dist/db/store.d.ts +36 -32
- package/dist/db/store.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +4 -4
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1268 -1639
- package/dist/lib/app-home.d.ts +48 -0
- package/dist/lib/app-home.d.ts.map +1 -0
- package/dist/lib/client-storage-policy.d.ts +10 -0
- package/dist/lib/client-storage-policy.d.ts.map +1 -0
- package/dist/lib/client-types.d.ts +184 -0
- package/dist/lib/client-types.d.ts.map +1 -0
- package/dist/lib/cloudflare.d.ts.map +1 -1
- package/dist/lib/cloudflare.js +23 -0
- package/dist/lib/config.d.ts +8 -17
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/dns-plan.d.ts +1 -1
- package/dist/lib/dns-plan.d.ts.map +1 -1
- package/dist/lib/domains-resolver.d.ts +96 -0
- package/dist/lib/domains-resolver.d.ts.map +1 -0
- package/dist/lib/registrar.d.ts +8 -0
- package/dist/lib/registrar.d.ts.map +1 -1
- package/dist/lib/registrar.js +23 -0
- package/dist/mcp/harness.d.ts +46 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/http.d.ts +3 -2
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +23611 -24591
- package/dist/mcp/tool-filter.d.ts +6 -0
- package/dist/mcp/tool-filter.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +21 -10
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +861 -245
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +84 -29
- package/dist/server/migrations.d.ts +35 -0
- package/dist/server/migrations.d.ts.map +1 -1
- package/package.json +8 -8
- package/postinstall.js +41 -0
package/dist/sdk/index.js
CHANGED
|
@@ -1,168 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/sdk/client.ts
|
|
3
|
-
class ApiError extends Error {
|
|
4
|
-
status;
|
|
5
|
-
body;
|
|
6
|
-
constructor(status, message, body) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.status = status;
|
|
9
|
-
this.body = body;
|
|
10
|
-
this.name = "ApiError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
class DomainsClient {
|
|
15
|
-
baseUrl;
|
|
16
|
-
apiKey;
|
|
17
|
-
fetchImpl;
|
|
18
|
-
baseHeaders;
|
|
19
|
-
constructor(options) {
|
|
20
|
-
if (!options.baseUrl)
|
|
21
|
-
throw new Error("DomainsClient requires a baseUrl.");
|
|
22
|
-
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
23
|
-
this.apiKey = options.apiKey;
|
|
24
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
25
|
-
this.baseHeaders = options.headers ?? {};
|
|
26
|
-
}
|
|
27
|
-
async request(method, path, opts) {
|
|
28
|
-
const url = new URL(this.baseUrl + path);
|
|
29
|
-
if (opts.query) {
|
|
30
|
-
for (const [key, value] of Object.entries(opts.query)) {
|
|
31
|
-
if (value !== undefined && value !== null)
|
|
32
|
-
url.searchParams.set(key, String(value));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
36
|
-
if (this.apiKey)
|
|
37
|
-
headers["x-api-key"] = this.apiKey;
|
|
38
|
-
let payload;
|
|
39
|
-
if (opts.body !== undefined) {
|
|
40
|
-
headers["Content-Type"] = "application/json";
|
|
41
|
-
payload = JSON.stringify(opts.body);
|
|
42
|
-
}
|
|
43
|
-
const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
|
|
44
|
-
const text = await response.text();
|
|
45
|
-
const data = text ? (() => {
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(text);
|
|
48
|
-
} catch {
|
|
49
|
-
return text;
|
|
50
|
-
}
|
|
51
|
-
})() : undefined;
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
|
|
54
|
-
}
|
|
55
|
-
return data;
|
|
56
|
-
}
|
|
57
|
-
async getHealth(init) {
|
|
58
|
-
return this.request("GET", `/health`, {
|
|
59
|
-
body: undefined,
|
|
60
|
-
query: undefined,
|
|
61
|
-
init
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
async getReady(init) {
|
|
65
|
-
return this.request("GET", `/ready`, {
|
|
66
|
-
body: undefined,
|
|
67
|
-
query: undefined,
|
|
68
|
-
init
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
async getDnsRecord(id, init) {
|
|
72
|
-
return this.request("GET", `/v1/dns/${encodeURIComponent(String(id))}`, {
|
|
73
|
-
body: undefined,
|
|
74
|
-
query: undefined,
|
|
75
|
-
init
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
async deleteDnsRecord(id, init) {
|
|
79
|
-
return this.request("DELETE", `/v1/dns/${encodeURIComponent(String(id))}`, {
|
|
80
|
-
body: undefined,
|
|
81
|
-
query: undefined,
|
|
82
|
-
init
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
async listDomains(query, init) {
|
|
86
|
-
return this.request("GET", `/v1/domains`, {
|
|
87
|
-
body: undefined,
|
|
88
|
-
query,
|
|
89
|
-
init
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
async createDomain(body, init) {
|
|
93
|
-
return this.request("POST", `/v1/domains`, {
|
|
94
|
-
body,
|
|
95
|
-
query: undefined,
|
|
96
|
-
init
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
async getDomain(id, init) {
|
|
100
|
-
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
101
|
-
body: undefined,
|
|
102
|
-
query: undefined,
|
|
103
|
-
init
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
async deleteDomain(id, init) {
|
|
107
|
-
return this.request("DELETE", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
108
|
-
body: undefined,
|
|
109
|
-
query: undefined,
|
|
110
|
-
init
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
async updateDomain(id, body, init) {
|
|
114
|
-
return this.request("PATCH", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
115
|
-
body,
|
|
116
|
-
query: undefined,
|
|
117
|
-
init
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
async listDnsRecords(id, init) {
|
|
121
|
-
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
|
|
122
|
-
body: undefined,
|
|
123
|
-
query: undefined,
|
|
124
|
-
init
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
async createDnsRecord(id, body, init) {
|
|
128
|
-
return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
|
|
129
|
-
body,
|
|
130
|
-
query: undefined,
|
|
131
|
-
init
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
async listOffers(id, init) {
|
|
135
|
-
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
|
|
136
|
-
body: undefined,
|
|
137
|
-
query: undefined,
|
|
138
|
-
init
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
async createOffer(id, body, init) {
|
|
142
|
-
return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
|
|
143
|
-
body,
|
|
144
|
-
query: undefined,
|
|
145
|
-
init
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async getDomainStats(init) {
|
|
149
|
-
return this.request("GET", `/v1/stats`, {
|
|
150
|
-
body: undefined,
|
|
151
|
-
query: undefined,
|
|
152
|
-
init
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
async getVersion(init) {
|
|
156
|
-
return this.request("GET", `/version`, {
|
|
157
|
-
body: undefined,
|
|
158
|
-
query: undefined,
|
|
159
|
-
init
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
2
|
// ../contracts/dist/client/transport.js
|
|
164
|
-
import {
|
|
165
|
-
import {
|
|
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";
|
|
166
10
|
function envToken(name) {
|
|
167
11
|
return name.toUpperCase().replace(/-/g, "_");
|
|
168
12
|
}
|
|
@@ -177,6 +21,9 @@ function credentialOverrideEnvKey(name) {
|
|
|
177
21
|
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
178
22
|
}
|
|
179
23
|
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
24
|
+
function credentialPointerEnvKey(name) {
|
|
25
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
26
|
+
}
|
|
180
27
|
|
|
181
28
|
class CredentialResolutionError extends Error {
|
|
182
29
|
appName;
|
|
@@ -188,34 +35,70 @@ class CredentialResolutionError extends Error {
|
|
|
188
35
|
this.attempted = attempted;
|
|
189
36
|
}
|
|
190
37
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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;
|
|
195
57
|
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
196
58
|
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
197
59
|
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
198
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,}$/;
|
|
199
62
|
function homeDir(env) {
|
|
200
63
|
const home = env.HOME?.trim();
|
|
201
64
|
return home ? home : null;
|
|
202
65
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
66
|
+
function absoluteOverride(env, key) {
|
|
67
|
+
const value = env[key]?.trim();
|
|
68
|
+
return value && isAbsolute(value) ? value : null;
|
|
205
69
|
}
|
|
206
|
-
function
|
|
70
|
+
function hasnaHomeDir(env) {
|
|
71
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
72
|
+
if (override)
|
|
73
|
+
return override;
|
|
207
74
|
const home = homeDir(env);
|
|
208
|
-
|
|
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))
|
|
209
86
|
return [];
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
216
98
|
}
|
|
217
99
|
function parseEnvFile(text) {
|
|
218
100
|
const values = new Map;
|
|
101
|
+
const unusable = new Set;
|
|
219
102
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
220
103
|
const line = rawLine.trim();
|
|
221
104
|
if (line.length === 0 || line.startsWith("#"))
|
|
@@ -230,40 +113,111 @@ function parseEnvFile(text) {
|
|
|
230
113
|
let value = withoutExport.slice(equals + 1).trim();
|
|
231
114
|
const quote = value[0];
|
|
232
115
|
if (quote === '"' || quote === "'") {
|
|
233
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
116
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
117
|
+
unusable.add(key);
|
|
234
118
|
continue;
|
|
119
|
+
}
|
|
235
120
|
value = value.slice(1, -1);
|
|
236
121
|
}
|
|
237
|
-
if (value.length === 0)
|
|
122
|
+
if (value.trim().length === 0) {
|
|
123
|
+
unusable.add(key);
|
|
238
124
|
continue;
|
|
125
|
+
}
|
|
126
|
+
if (values.has(key) && values.get(key) !== value)
|
|
127
|
+
unusable.add(key);
|
|
239
128
|
values.set(key, value);
|
|
240
129
|
}
|
|
241
|
-
return values;
|
|
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;
|
|
242
138
|
}
|
|
243
139
|
function readAppConfigFile(path) {
|
|
244
|
-
|
|
140
|
+
const unsafe = (reason) => {
|
|
141
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
142
|
+
};
|
|
143
|
+
let fd = -1;
|
|
245
144
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
145
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const code = error.code;
|
|
148
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
248
149
|
return null;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
252
175
|
}
|
|
253
|
-
return parseEnvFile(text);
|
|
254
176
|
}
|
|
255
177
|
function readCredentialFile(path, apiKeyKeys) {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
178
|
+
const parsed = readAppConfigFile(path);
|
|
179
|
+
if (!parsed)
|
|
258
180
|
return null;
|
|
259
181
|
for (const key of apiKeyKeys) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
214
|
}
|
|
264
215
|
return null;
|
|
265
216
|
}
|
|
266
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
|
+
}
|
|
267
221
|
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
268
222
|
return;
|
|
269
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]);
|
|
@@ -276,7 +230,6 @@ function sealCredential(fields) {
|
|
|
276
230
|
tier: fields.tier,
|
|
277
231
|
source: fields.source,
|
|
278
232
|
deliberate: fields.deliberate,
|
|
279
|
-
deprecated: fields.deprecated,
|
|
280
233
|
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
281
234
|
warning: fields.warning
|
|
282
235
|
};
|
|
@@ -287,6 +240,14 @@ function sealCredential(fields) {
|
|
|
287
240
|
writable: false,
|
|
288
241
|
configurable: false
|
|
289
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
|
+
}
|
|
290
251
|
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
291
252
|
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
292
253
|
enumerable: false,
|
|
@@ -303,46 +264,140 @@ function sealCredential(fields) {
|
|
|
303
264
|
}
|
|
304
265
|
function firstEnvValue(env, keys) {
|
|
305
266
|
for (const key of keys) {
|
|
267
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
268
|
+
continue;
|
|
306
269
|
const value = env[key]?.trim();
|
|
307
270
|
if (value)
|
|
308
271
|
return { key, value };
|
|
309
272
|
}
|
|
310
273
|
return null;
|
|
311
274
|
}
|
|
312
|
-
var
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
const existing = host[DEPRECATION_REGISTRY];
|
|
316
|
-
if (existing instanceof Set)
|
|
317
|
-
return existing;
|
|
318
|
-
const created = new Set;
|
|
319
|
-
host[DEPRECATION_REGISTRY] = created;
|
|
320
|
-
return created;
|
|
275
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
276
|
+
function isAmbientEnvironment(env) {
|
|
277
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
321
278
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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]);
|
|
326
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);
|
|
327
379
|
}
|
|
328
380
|
function resolveCredential(name, env, options = {}) {
|
|
381
|
+
env = snapshotClientEnvironment(name, env);
|
|
329
382
|
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
330
383
|
const diskPaths = credentialDiskSources(name, env);
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
}
|
|
333
389
|
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
334
390
|
return sealCredential({
|
|
335
391
|
apiKey: explicitKey,
|
|
336
392
|
tier: "argument",
|
|
337
393
|
source: "explicit apiKey argument",
|
|
338
394
|
deliberate: true,
|
|
339
|
-
deprecated: false,
|
|
340
395
|
diskCandidates: diskPaths,
|
|
341
396
|
warning: null
|
|
342
397
|
});
|
|
343
398
|
}
|
|
344
399
|
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
345
|
-
const overrideRaw = env[overrideKeyName];
|
|
400
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
346
401
|
if (overrideRaw !== undefined) {
|
|
347
402
|
const override = overrideRaw.trim();
|
|
348
403
|
if (!override) {
|
|
@@ -354,12 +409,38 @@ function resolveCredential(name, env, options = {}) {
|
|
|
354
409
|
tier: "override",
|
|
355
410
|
source: overrideKeyName,
|
|
356
411
|
deliberate: true,
|
|
357
|
-
deprecated: false,
|
|
358
412
|
diskCandidates: diskPaths,
|
|
359
413
|
warning: null
|
|
360
414
|
});
|
|
361
415
|
}
|
|
362
|
-
const
|
|
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();
|
|
363
444
|
if (profile) {
|
|
364
445
|
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
365
446
|
if (!SAFE_PROFILE.test(profile)) {
|
|
@@ -375,7 +456,6 @@ function resolveCredential(name, env, options = {}) {
|
|
|
375
456
|
tier: "profile",
|
|
376
457
|
source: path,
|
|
377
458
|
deliberate: true,
|
|
378
|
-
deprecated: false,
|
|
379
459
|
diskCandidates: paths,
|
|
380
460
|
warning: null
|
|
381
461
|
});
|
|
@@ -383,51 +463,316 @@ function resolveCredential(name, env, options = {}) {
|
|
|
383
463
|
}
|
|
384
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);
|
|
385
465
|
}
|
|
386
|
-
const
|
|
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);
|
|
387
490
|
if (diskHits.length > 0) {
|
|
388
491
|
const winner = diskHits[0];
|
|
389
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
492
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
390
493
|
const divergentSources = [
|
|
391
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
392
|
-
...
|
|
393
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
394
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
395
|
-
})()
|
|
494
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
495
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
396
496
|
];
|
|
397
|
-
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.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;
|
|
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;
|
|
398
498
|
return sealCredential({
|
|
399
499
|
apiKey: winner.value,
|
|
400
|
-
tier:
|
|
401
|
-
source: winner.path,
|
|
500
|
+
tier: winner.src.tier,
|
|
501
|
+
source: winner.src.path,
|
|
402
502
|
deliberate: false,
|
|
403
|
-
deprecated: false,
|
|
404
503
|
diskCandidates: diskPaths,
|
|
405
504
|
warning
|
|
406
505
|
});
|
|
407
506
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
assertUsableCredential(name, legacy.key, legacy.value);
|
|
411
|
-
const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
|
|
412
|
-
const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
|
|
413
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
414
|
-
const notified = deprecationNotified();
|
|
415
|
-
if (!notified.has(name)) {
|
|
416
|
-
notified.add(name);
|
|
417
|
-
sink(message);
|
|
418
|
-
}
|
|
507
|
+
if (envHit) {
|
|
508
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
419
509
|
return sealCredential({
|
|
420
|
-
apiKey:
|
|
421
|
-
tier: "
|
|
422
|
-
source:
|
|
510
|
+
apiKey: envHit.value,
|
|
511
|
+
tier: "env",
|
|
512
|
+
source: envHit.key,
|
|
423
513
|
deliberate: false,
|
|
424
|
-
deprecated: true,
|
|
425
514
|
diskCandidates: diskPaths,
|
|
426
|
-
warning:
|
|
515
|
+
warning: null
|
|
427
516
|
});
|
|
428
517
|
}
|
|
429
518
|
return null;
|
|
430
519
|
}
|
|
520
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
521
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
522
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
523
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
524
|
+
const pointerEnvKey = pointerResolution.source;
|
|
525
|
+
if (!vaultKey) {
|
|
526
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
527
|
+
}
|
|
528
|
+
let secretsSdk;
|
|
529
|
+
try {
|
|
530
|
+
secretsSdk = requireSecretsSdk(SECRETS_PACKAGE_SPECIFIER);
|
|
531
|
+
} catch {
|
|
532
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
533
|
+
}
|
|
534
|
+
let client;
|
|
535
|
+
try {
|
|
536
|
+
client = secretsSdk.createSecretsClientFromEnv(env);
|
|
537
|
+
} catch {
|
|
538
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
539
|
+
}
|
|
540
|
+
let secret;
|
|
541
|
+
try {
|
|
542
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
543
|
+
} catch {
|
|
544
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
545
|
+
}
|
|
546
|
+
const value = secret.value;
|
|
547
|
+
if (!value) {
|
|
548
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
549
|
+
}
|
|
550
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
551
|
+
return sealCredential({
|
|
552
|
+
apiKey: value,
|
|
553
|
+
tier: "pointer",
|
|
554
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
555
|
+
deliberate: true,
|
|
556
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
557
|
+
warning: null
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
561
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
562
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
563
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
564
|
+
}
|
|
565
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
566
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
567
|
+
function isValidDnsDomain(value) {
|
|
568
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
572
|
+
}
|
|
573
|
+
function validateAppSlug(name) {
|
|
574
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
575
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
576
|
+
}
|
|
577
|
+
return name;
|
|
578
|
+
}
|
|
579
|
+
function rawAuthority(value) {
|
|
580
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
581
|
+
if (!match)
|
|
582
|
+
throw new Error("API URL must be absolute.");
|
|
583
|
+
const afterScheme = value.slice(match[0].length);
|
|
584
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
585
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
586
|
+
if (!authority)
|
|
587
|
+
throw new Error("API URL must include a hostname.");
|
|
588
|
+
return authority;
|
|
589
|
+
}
|
|
590
|
+
function assertCanonicalPort(port) {
|
|
591
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
592
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
593
|
+
}
|
|
594
|
+
const numericPort = Number(port);
|
|
595
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
596
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function canonicalAuthorityHostname(authority) {
|
|
600
|
+
let rawHostname;
|
|
601
|
+
if (authority.startsWith("[")) {
|
|
602
|
+
const closingBracket = authority.indexOf("]");
|
|
603
|
+
if (closingBracket === -1) {
|
|
604
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
605
|
+
}
|
|
606
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
607
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
608
|
+
if (portSuffix) {
|
|
609
|
+
if (!portSuffix.startsWith(":")) {
|
|
610
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
611
|
+
}
|
|
612
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
613
|
+
}
|
|
614
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
615
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
const firstColon = authority.indexOf(":");
|
|
619
|
+
const lastColon = authority.lastIndexOf(":");
|
|
620
|
+
if (firstColon !== lastColon) {
|
|
621
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
622
|
+
}
|
|
623
|
+
if (lastColon !== -1) {
|
|
624
|
+
const port = authority.slice(lastColon + 1);
|
|
625
|
+
assertCanonicalPort(port);
|
|
626
|
+
rawHostname = authority.slice(0, lastColon);
|
|
627
|
+
} else {
|
|
628
|
+
rawHostname = authority;
|
|
629
|
+
}
|
|
630
|
+
const ipVersion = isIP(rawHostname);
|
|
631
|
+
const numericAddressParts = rawHostname.split(".");
|
|
632
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
633
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
634
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return rawHostname.toLowerCase();
|
|
638
|
+
}
|
|
639
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
640
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
641
|
+
}
|
|
642
|
+
function toV1BaseUrl(apiUrl) {
|
|
643
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
644
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
645
|
+
}
|
|
646
|
+
const input = apiUrl.trim();
|
|
647
|
+
const authority = rawAuthority(input);
|
|
648
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
649
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
650
|
+
}
|
|
651
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
652
|
+
const url = new URL(input);
|
|
653
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
654
|
+
throw new Error("API URL must use http or https.");
|
|
655
|
+
}
|
|
656
|
+
if (url.username || url.password) {
|
|
657
|
+
throw new Error("API URL must not include credentials.");
|
|
658
|
+
}
|
|
659
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
660
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
661
|
+
}
|
|
662
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
663
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
664
|
+
}
|
|
665
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
666
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
667
|
+
}
|
|
668
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
669
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
670
|
+
}
|
|
671
|
+
if (url.search || url.hash) {
|
|
672
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
673
|
+
}
|
|
674
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
675
|
+
if (path.endsWith("/v1"))
|
|
676
|
+
path = path.slice(0, -"/v1".length);
|
|
677
|
+
url.pathname = `${path}/v1`;
|
|
678
|
+
return url.toString().replace(/\/+$/, "");
|
|
679
|
+
}
|
|
680
|
+
class ClientTransportConfigurationError extends Error {
|
|
681
|
+
appName;
|
|
682
|
+
sources;
|
|
683
|
+
constructor(appName, message, sources = []) {
|
|
684
|
+
super(message);
|
|
685
|
+
this.name = "ClientTransportConfigurationError";
|
|
686
|
+
this.appName = appName;
|
|
687
|
+
this.sources = Object.freeze([...sources]);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
691
|
+
env = snapshotClientEnvironment(name, env);
|
|
692
|
+
const keys = clientTransportEnvKeys(name);
|
|
693
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
694
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
695
|
+
if (blankUrl) {
|
|
696
|
+
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]);
|
|
697
|
+
}
|
|
698
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
699
|
+
if (controlledUrl) {
|
|
700
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
701
|
+
}
|
|
702
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
703
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
704
|
+
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));
|
|
705
|
+
}
|
|
706
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
707
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
708
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
709
|
+
if (diskConfigUrlHit?.unusable) {
|
|
710
|
+
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]);
|
|
711
|
+
}
|
|
712
|
+
const urlCandidates = [
|
|
713
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
714
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
715
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
716
|
+
];
|
|
717
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
718
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
719
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
720
|
+
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));
|
|
721
|
+
}
|
|
722
|
+
const warnings = [];
|
|
723
|
+
if (configuredUrl && !envUrlHit) {
|
|
724
|
+
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.`);
|
|
725
|
+
}
|
|
726
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
727
|
+
if (!credential) {
|
|
728
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
729
|
+
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`;
|
|
730
|
+
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.`);
|
|
731
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
732
|
+
}
|
|
733
|
+
if (credential.warning)
|
|
734
|
+
warnings.push(credential.warning);
|
|
735
|
+
let urlHit;
|
|
736
|
+
if (configuredUrl) {
|
|
737
|
+
urlHit = configuredUrl;
|
|
738
|
+
} else {
|
|
739
|
+
try {
|
|
740
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
743
|
+
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]]);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const apiUrlSource = urlHit.key;
|
|
747
|
+
let baseUrl;
|
|
748
|
+
try {
|
|
749
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
resolution: {
|
|
756
|
+
transport: "http",
|
|
757
|
+
transportSource: urlHit.key,
|
|
758
|
+
baseUrl,
|
|
759
|
+
apiUrlSource,
|
|
760
|
+
apiKeyPresent: true,
|
|
761
|
+
apiKeySource: credential.source,
|
|
762
|
+
apiKeyTier: credential.tier,
|
|
763
|
+
misconfigured: false,
|
|
764
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
765
|
+
},
|
|
766
|
+
credential
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
770
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
771
|
+
}
|
|
772
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
773
|
+
const paths = credentialDiskSources(name, env);
|
|
774
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
775
|
+
}
|
|
431
776
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
432
777
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
433
778
|
"host",
|
|
@@ -437,14 +782,285 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
437
782
|
"x-original-host"
|
|
438
783
|
]);
|
|
439
784
|
|
|
785
|
+
// src/lib/client-storage-policy.ts
|
|
786
|
+
var LOCAL_PATH_VARS = [
|
|
787
|
+
"HASNA_DOMAINS_DB_PATH",
|
|
788
|
+
"DOMAINS_DB_PATH",
|
|
789
|
+
"HASNA_DOMAINS_DIR",
|
|
790
|
+
"DOMAINS_DIR"
|
|
791
|
+
];
|
|
792
|
+
function explicitLocalPathVar(env = process.env) {
|
|
793
|
+
return LOCAL_PATH_VARS.find((key) => (env[key] ?? "").trim() !== "");
|
|
794
|
+
}
|
|
795
|
+
function domainsAuthorityEnvKeys() {
|
|
796
|
+
const keys = clientTransportEnvKeys("domains");
|
|
797
|
+
return [
|
|
798
|
+
...keys.apiUrlKeys,
|
|
799
|
+
...keys.apiKeyKeys,
|
|
800
|
+
credentialOverrideEnvKey("domains"),
|
|
801
|
+
credentialPointerEnvKey("domains"),
|
|
802
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
function assertDomainsClientStorage(env = process.env) {
|
|
806
|
+
const pathVar = explicitLocalPathVar(env);
|
|
807
|
+
if (pathVar)
|
|
808
|
+
throw new Error(`domains: ${pathVar} is no longer supported by clients. Unset local database path variables and configure the shared API with HASNA_DOMAINS_API_KEY or saved credentials. Preserve any existing database until its records have been migrated and verified.`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/sdk/client.ts
|
|
812
|
+
class ApiError extends Error {
|
|
813
|
+
status;
|
|
814
|
+
body;
|
|
815
|
+
constructor(status, message, body) {
|
|
816
|
+
super(message);
|
|
817
|
+
this.status = status;
|
|
818
|
+
this.body = body;
|
|
819
|
+
this.name = "ApiError";
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
class DomainsClient {
|
|
824
|
+
baseUrl;
|
|
825
|
+
apiKey;
|
|
826
|
+
fetchImpl;
|
|
827
|
+
baseHeaders;
|
|
828
|
+
constructor(options) {
|
|
829
|
+
if (!options.baseUrl)
|
|
830
|
+
throw new Error("DomainsClient requires a baseUrl.");
|
|
831
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
832
|
+
this.apiKey = options.apiKey;
|
|
833
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
834
|
+
this.baseHeaders = options.headers ?? {};
|
|
835
|
+
}
|
|
836
|
+
async request(method, path, opts) {
|
|
837
|
+
const url = new URL(this.baseUrl + path);
|
|
838
|
+
if (opts.query) {
|
|
839
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
840
|
+
if (value !== undefined && value !== null)
|
|
841
|
+
url.searchParams.set(key, String(value));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
845
|
+
if (this.apiKey)
|
|
846
|
+
headers["x-api-key"] = this.apiKey;
|
|
847
|
+
let payload;
|
|
848
|
+
if (opts.body !== undefined) {
|
|
849
|
+
headers["Content-Type"] = "application/json";
|
|
850
|
+
payload = JSON.stringify(opts.body);
|
|
851
|
+
}
|
|
852
|
+
const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
|
|
853
|
+
const text = await response.text();
|
|
854
|
+
const data = text ? (() => {
|
|
855
|
+
try {
|
|
856
|
+
return JSON.parse(text);
|
|
857
|
+
} catch {
|
|
858
|
+
return text;
|
|
859
|
+
}
|
|
860
|
+
})() : undefined;
|
|
861
|
+
if (!response.ok) {
|
|
862
|
+
throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
|
|
863
|
+
}
|
|
864
|
+
return data;
|
|
865
|
+
}
|
|
866
|
+
async getHealth(init) {
|
|
867
|
+
return this.request("GET", `/health`, {
|
|
868
|
+
body: undefined,
|
|
869
|
+
query: undefined,
|
|
870
|
+
init
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
async getReady(init) {
|
|
874
|
+
return this.request("GET", `/ready`, {
|
|
875
|
+
body: undefined,
|
|
876
|
+
query: undefined,
|
|
877
|
+
init
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async getDnsRecord(id, init) {
|
|
881
|
+
return this.request("GET", `/v1/dns/${encodeURIComponent(String(id))}`, {
|
|
882
|
+
body: undefined,
|
|
883
|
+
query: undefined,
|
|
884
|
+
init
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async deleteDnsRecord(id, init) {
|
|
888
|
+
return this.request("DELETE", `/v1/dns/${encodeURIComponent(String(id))}`, {
|
|
889
|
+
body: undefined,
|
|
890
|
+
query: undefined,
|
|
891
|
+
init
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
async listDomains(query, init) {
|
|
895
|
+
return this.request("GET", `/v1/domains`, {
|
|
896
|
+
body: undefined,
|
|
897
|
+
query,
|
|
898
|
+
init
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async createDomain(body, init) {
|
|
902
|
+
return this.request("POST", `/v1/domains`, {
|
|
903
|
+
body,
|
|
904
|
+
query: undefined,
|
|
905
|
+
init
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
async getDomain(id, init) {
|
|
909
|
+
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
910
|
+
body: undefined,
|
|
911
|
+
query: undefined,
|
|
912
|
+
init
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
async deleteDomain(id, init) {
|
|
916
|
+
return this.request("DELETE", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
917
|
+
body: undefined,
|
|
918
|
+
query: undefined,
|
|
919
|
+
init
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
async updateDomain(id, body, init) {
|
|
923
|
+
return this.request("PATCH", `/v1/domains/${encodeURIComponent(String(id))}`, {
|
|
924
|
+
body,
|
|
925
|
+
query: undefined,
|
|
926
|
+
init
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
async listDnsRecords(id, init) {
|
|
930
|
+
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
|
|
931
|
+
body: undefined,
|
|
932
|
+
query: undefined,
|
|
933
|
+
init
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
async createDnsRecord(id, body, init) {
|
|
937
|
+
return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
|
|
938
|
+
body,
|
|
939
|
+
query: undefined,
|
|
940
|
+
init
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
async listOffers(id, init) {
|
|
944
|
+
return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
|
|
945
|
+
body: undefined,
|
|
946
|
+
query: undefined,
|
|
947
|
+
init
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
async createOffer(id, body, init) {
|
|
951
|
+
return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
|
|
952
|
+
body,
|
|
953
|
+
query: undefined,
|
|
954
|
+
init
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
async getDomainStats(init) {
|
|
958
|
+
return this.request("GET", `/v1/stats`, {
|
|
959
|
+
body: undefined,
|
|
960
|
+
query: undefined,
|
|
961
|
+
init
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
async getVersion(init) {
|
|
965
|
+
return this.request("GET", `/version`, {
|
|
966
|
+
body: undefined,
|
|
967
|
+
query: undefined,
|
|
968
|
+
init
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// ../contracts/dist/client/storage.js
|
|
973
|
+
import { createRequire as createRequire2 } from "module";
|
|
974
|
+
var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
975
|
+
var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
976
|
+
var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
977
|
+
var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
978
|
+
var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
|
|
979
|
+
var requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
980
|
+
var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
981
|
+
var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
982
|
+
"host",
|
|
983
|
+
":authority",
|
|
984
|
+
"forwarded",
|
|
985
|
+
"x-forwarded-host",
|
|
986
|
+
"x-original-host"
|
|
987
|
+
]);
|
|
988
|
+
|
|
989
|
+
// src/lib/domains-resolver.ts
|
|
990
|
+
var DOMAINS_APP_NAME = "domains";
|
|
991
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
992
|
+
function isAmbientEnv(env) {
|
|
993
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
994
|
+
return true;
|
|
995
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
996
|
+
}
|
|
997
|
+
function domainsResolverEnv(env) {
|
|
998
|
+
const blanks = domainsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
999
|
+
if (blanks.length === 0)
|
|
1000
|
+
return env;
|
|
1001
|
+
const next = { ...env };
|
|
1002
|
+
for (const key of blanks)
|
|
1003
|
+
delete next[key];
|
|
1004
|
+
return next;
|
|
1005
|
+
}
|
|
1006
|
+
function domainsResolverInputs(env, credentials = {}) {
|
|
1007
|
+
assertDomainsClientStorage(env);
|
|
1008
|
+
const normalised = domainsResolverEnv(env);
|
|
1009
|
+
if (normalised === env)
|
|
1010
|
+
return { env: normalised, credentials };
|
|
1011
|
+
const keychain = { ...credentials.keychain };
|
|
1012
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
1013
|
+
keychain.enabled = isAmbientEnv(env);
|
|
1014
|
+
}
|
|
1015
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
440
1018
|
// src/sdk/index.ts
|
|
441
1019
|
function createDomainsClientFromEnv(env = process.env, overrides = {}) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1020
|
+
assertDomainsClientStorage(env);
|
|
1021
|
+
const { baseUrl: baseUrlOverride, apiKey: apiKeyOverride, profile, keychain, ...rest } = overrides;
|
|
1022
|
+
if (baseUrlOverride !== undefined) {
|
|
1023
|
+
return new DomainsClient({
|
|
1024
|
+
baseUrl: baseUrlOverride,
|
|
1025
|
+
...apiKeyOverride !== undefined ? { apiKey: apiKeyOverride } : {},
|
|
1026
|
+
...rest
|
|
1027
|
+
});
|
|
445
1028
|
}
|
|
446
|
-
const
|
|
447
|
-
|
|
1029
|
+
const credentials = {
|
|
1030
|
+
...apiKeyOverride !== undefined ? { apiKey: apiKeyOverride } : {},
|
|
1031
|
+
...profile ? { profile } : {},
|
|
1032
|
+
...keychain ? { keychain } : {}
|
|
1033
|
+
};
|
|
1034
|
+
const { env: resolverEnv, credentials: resolverCredentials } = domainsResolverInputs(env, credentials);
|
|
1035
|
+
const resolution = resolveClientTransport(DOMAINS_APP_NAME, resolverEnv, {
|
|
1036
|
+
credentials: resolverCredentials
|
|
1037
|
+
});
|
|
1038
|
+
const credential = resolveCredential(DOMAINS_APP_NAME, resolverEnv, resolverCredentials);
|
|
1039
|
+
if (!credential) {
|
|
1040
|
+
throw new Error("domains SDK: no API key resolved from any credential tier; refusing to build an unauthenticated client. " + "Looked at HASNA_DOMAINS_API_KEY_OVERRIDE / HASNA_PROFILE / HASNA_DOMAINS_API_KEY_REF, the Keychain item " + "hasna.credentials.domains.api-key, ~/.hasna/domains/config/credentials, then HASNA_DOMAINS_API_KEY.");
|
|
1041
|
+
}
|
|
1042
|
+
const baseUrl = resolution.baseUrl.replace(/\/v1$/, "");
|
|
1043
|
+
const baseFetch = rest.fetch ?? ((input, init) => fetch(input, init));
|
|
1044
|
+
const fetchWithFreshCredential = async (input, init) => {
|
|
1045
|
+
const headers = {};
|
|
1046
|
+
new Headers(init?.headers ?? {}).forEach((value, key) => {
|
|
1047
|
+
headers[key] = value;
|
|
1048
|
+
});
|
|
1049
|
+
try {
|
|
1050
|
+
const fresh = resolveCredential(DOMAINS_APP_NAME, resolverEnv, resolverCredentials);
|
|
1051
|
+
if (fresh) {
|
|
1052
|
+
const key = fresh.tier === "pointer" ? (await completePointerCredential(DOMAINS_APP_NAME, fresh, resolverEnv)).apiKey : fresh.apiKey;
|
|
1053
|
+
headers["x-api-key"] = key;
|
|
1054
|
+
}
|
|
1055
|
+
} catch {}
|
|
1056
|
+
return baseFetch(input, { ...init, headers });
|
|
1057
|
+
};
|
|
1058
|
+
return new DomainsClient({
|
|
1059
|
+
baseUrl,
|
|
1060
|
+
apiKey: credential.apiKey,
|
|
1061
|
+
...rest,
|
|
1062
|
+
fetch: fetchWithFreshCredential
|
|
1063
|
+
});
|
|
448
1064
|
}
|
|
449
1065
|
export {
|
|
450
1066
|
createDomainsClientFromEnv,
|