@dnsid-ai/sdk 0.20.1
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/LICENSE +201 -0
- package/README.md +121 -0
- package/dist/index.cjs +657 -0
- package/dist/index.d.cts +210 -0
- package/dist/index.d.ts +210 -0
- package/dist/index.js +625 -0
- package/dist/node.cjs +560 -0
- package/dist/node.d.cts +195 -0
- package/dist/node.d.ts +195 -0
- package/dist/node.js +518 -0
- package/package.json +61 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// src/node.ts
|
|
2
|
+
import * as fs3 from "fs/promises";
|
|
3
|
+
import * as os2 from "os";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
import { IdentityManager, ArgumentError as ArgumentError2, normalizeFQDN as normalizeFQDN2, validateDnsidConfig } from "@dnsid-ai/protocol";
|
|
6
|
+
import { DEFAULT_REGISTRY_URL as DEFAULT_REGISTRY_URL2 } from "@dnsid-ai/registry";
|
|
7
|
+
|
|
8
|
+
// src/local-key-provider.ts
|
|
9
|
+
import * as fs2 from "fs/promises";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { createPrivateKey } from "crypto";
|
|
13
|
+
import { ArgumentError, jwkSignatureAlg, jwkThumbprint, normalizeFQDN, verifyWithKey } from "@dnsid-ai/protocol";
|
|
14
|
+
|
|
15
|
+
// src/node-fs.ts
|
|
16
|
+
import * as fs from "fs/promises";
|
|
17
|
+
async function readJson(filePath) {
|
|
18
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw new Error(`${filePath}: ${e.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/local-key-provider.ts
|
|
27
|
+
var LocalKeyProvider = class _LocalKeyProvider {
|
|
28
|
+
store;
|
|
29
|
+
filePath;
|
|
30
|
+
// ponytail: per-instance queue; use a cross-process lock and reload before writes if shared writers are needed.
|
|
31
|
+
mutations = Promise.resolve();
|
|
32
|
+
constructor(store, filePath) {
|
|
33
|
+
this.store = store;
|
|
34
|
+
this.filePath = filePath;
|
|
35
|
+
}
|
|
36
|
+
static async load(filePath, createIfMissing = false, algorithm = "EdDSA") {
|
|
37
|
+
filePath = path.resolve(filePath);
|
|
38
|
+
let store;
|
|
39
|
+
try {
|
|
40
|
+
filePath = await fs2.realpath(filePath);
|
|
41
|
+
store = await readJson(filePath);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e.code !== "ENOENT" || !createIfMissing) throw e;
|
|
44
|
+
store = await buildInitialStore(algorithm);
|
|
45
|
+
const createdDirectory = await fs2.mkdir(path.dirname(filePath), { recursive: true });
|
|
46
|
+
try {
|
|
47
|
+
await writeStore(filePath, store, void 0, true);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code !== "EEXIST") throw error;
|
|
50
|
+
store = await readJson(filePath);
|
|
51
|
+
}
|
|
52
|
+
if (createdDirectory) {
|
|
53
|
+
const parent = path.dirname(createdDirectory);
|
|
54
|
+
for (let dir = path.dirname(filePath); ; dir = path.dirname(dir)) {
|
|
55
|
+
await syncDirectory(dir);
|
|
56
|
+
if (dir === parent) break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return new _LocalKeyProvider(store, filePath);
|
|
61
|
+
}
|
|
62
|
+
static async fromEnvironment(env, options = {}) {
|
|
63
|
+
return _LocalKeyProvider.load(keyStorePathFromEnvironment(env, options.defaultPath), options.createIfMissing, options.algorithm);
|
|
64
|
+
}
|
|
65
|
+
static async generate(algorithm = "EdDSA") {
|
|
66
|
+
return new _LocalKeyProvider(await buildInitialStore(algorithm), null);
|
|
67
|
+
}
|
|
68
|
+
static async fromDirectory(dir) {
|
|
69
|
+
const jwkPath = path.join(dir, "private.jwk");
|
|
70
|
+
let key;
|
|
71
|
+
let keyPath = jwkPath;
|
|
72
|
+
try {
|
|
73
|
+
key = await readJson(jwkPath);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e.code !== "ENOENT") throw e;
|
|
76
|
+
keyPath = path.join(dir, "private.pem");
|
|
77
|
+
const pem = await fs2.readFile(keyPath);
|
|
78
|
+
key = createPrivateKey(pem).export({ format: "jwk" });
|
|
79
|
+
}
|
|
80
|
+
const active = await validateAndNormalizePrivateJwk(key, keyPath);
|
|
81
|
+
return new _LocalKeyProvider({ active, retained: [], pending: [] }, null);
|
|
82
|
+
}
|
|
83
|
+
/** Loads a single private JWK file, including DNSid CLI `entity_key_path` files. */
|
|
84
|
+
static async fromFile(filePath) {
|
|
85
|
+
const key = await readJson(filePath);
|
|
86
|
+
const active = await validateAndNormalizePrivateJwk(key, filePath);
|
|
87
|
+
return new _LocalKeyProvider({ active, retained: [], pending: [] }, null);
|
|
88
|
+
}
|
|
89
|
+
static async fromDomain(domain, dnsidDir = path.join(os.homedir(), ".dnsid")) {
|
|
90
|
+
return _LocalKeyProvider.fromDirectory(path.join(dnsidDir, normalizeFQDN(domain, true)));
|
|
91
|
+
}
|
|
92
|
+
async signingKey() {
|
|
93
|
+
return toPublicJwk(this.store.active);
|
|
94
|
+
}
|
|
95
|
+
async jwk(kid) {
|
|
96
|
+
if (this.store.active.kid === kid) return toPublicJwk(this.store.active);
|
|
97
|
+
const retained = this.store.retained.find((k) => k.kid === kid);
|
|
98
|
+
if (retained) return toPublicJwk(retained);
|
|
99
|
+
const pending = this.store.pending.find((k) => k.kid === kid);
|
|
100
|
+
if (pending) return toPublicJwk(pending);
|
|
101
|
+
throw new ArgumentError(`key not found: ${kid}`);
|
|
102
|
+
}
|
|
103
|
+
async listKeyIds() {
|
|
104
|
+
return [this.store.active.kid, ...this.store.retained.map((k) => k.kid)];
|
|
105
|
+
}
|
|
106
|
+
async sign(payload) {
|
|
107
|
+
return this.signKey(this.store.active.kid, payload);
|
|
108
|
+
}
|
|
109
|
+
async signKey(kid, payload) {
|
|
110
|
+
const key = kid === this.store.active.kid ? this.store.active : this.store.pending.find((candidate) => candidate.kid === kid);
|
|
111
|
+
if (!key) throw new ArgumentError(`key is not active or pending for signing: ${kid}`);
|
|
112
|
+
const cryptoKey = await importPrivateKey(key);
|
|
113
|
+
const sig = await crypto.subtle.sign(signAlgorithm(key), cryptoKey, toArrayBuffer(payload));
|
|
114
|
+
return new Uint8Array(sig);
|
|
115
|
+
}
|
|
116
|
+
async generateKey() {
|
|
117
|
+
return this.mutate(async (next) => {
|
|
118
|
+
const key = await generateStoredKey(localKeyAlgorithm(next.active));
|
|
119
|
+
next.pending.push(key);
|
|
120
|
+
return key.kid;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async activate(kid) {
|
|
124
|
+
await this.mutate((next) => {
|
|
125
|
+
const idx = next.pending.findIndex((k) => k.kid === kid);
|
|
126
|
+
if (idx === -1) throw new ArgumentError(`no pending key with kid "${kid}"`);
|
|
127
|
+
const [incoming] = next.pending.splice(idx, 1);
|
|
128
|
+
next.retained.push(next.active);
|
|
129
|
+
next.active = incoming;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async supersede(kid) {
|
|
133
|
+
await this.mutate((next) => {
|
|
134
|
+
if (next.active.kid === kid) {
|
|
135
|
+
throw new ArgumentError("cannot supersede the active key; activate a replacement first");
|
|
136
|
+
}
|
|
137
|
+
const idx = next.retained.findIndex((k) => k.kid === kid);
|
|
138
|
+
if (idx === -1) throw new ArgumentError(`no retained key with kid "${kid}"`);
|
|
139
|
+
next.retained.splice(idx, 1);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** @deprecated Use supersede(). */
|
|
143
|
+
async purge(kid) {
|
|
144
|
+
await this.supersede(kid);
|
|
145
|
+
}
|
|
146
|
+
mutate(change) {
|
|
147
|
+
const operation = this.mutations.then(async () => {
|
|
148
|
+
const next = structuredClone(this.store);
|
|
149
|
+
const result = await change(next);
|
|
150
|
+
if (this.filePath) {
|
|
151
|
+
await writeStore(`${this.filePath}.bak`, this.store);
|
|
152
|
+
await writeStore(this.filePath, next, () => {
|
|
153
|
+
this.store = next;
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
this.store = next;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
});
|
|
160
|
+
this.mutations = operation.catch(() => {
|
|
161
|
+
});
|
|
162
|
+
return operation;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
async function writeStore(filePath, store, committed, exclusive = false) {
|
|
166
|
+
const tempPath = `${filePath}.${crypto.randomUUID()}.tmp`;
|
|
167
|
+
const file = await fs2.open(tempPath, "wx", 384);
|
|
168
|
+
try {
|
|
169
|
+
try {
|
|
170
|
+
await file.writeFile(JSON.stringify(store, null, 2), "utf-8");
|
|
171
|
+
await file.sync();
|
|
172
|
+
} finally {
|
|
173
|
+
await file.close();
|
|
174
|
+
}
|
|
175
|
+
if (exclusive) await fs2.link(tempPath, filePath);
|
|
176
|
+
else await fs2.rename(tempPath, filePath);
|
|
177
|
+
committed?.();
|
|
178
|
+
await syncDirectory(path.dirname(filePath));
|
|
179
|
+
} finally {
|
|
180
|
+
await fs2.rm(tempPath, { force: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function syncDirectory(dir) {
|
|
184
|
+
const directory = await fs2.open(dir, "r");
|
|
185
|
+
try {
|
|
186
|
+
await directory.sync();
|
|
187
|
+
} finally {
|
|
188
|
+
await directory.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function keyStorePathFromEnvironment(env = process.env, defaultPath = ".dnsid/keys.json") {
|
|
192
|
+
return env["DNSID_KEY_STORE"] || env["DNSID_KEY_STORE_PATH"] || env["DNSID_KEYSTORE_PATH"] || defaultPath;
|
|
193
|
+
}
|
|
194
|
+
function toPublicJwk(key) {
|
|
195
|
+
const { d: _d, ...pub } = key;
|
|
196
|
+
return pub;
|
|
197
|
+
}
|
|
198
|
+
async function validateAndNormalizePrivateJwk(key, filePath) {
|
|
199
|
+
const isEd25519 = key.kty === "OKP" && key.crv === "Ed25519" && typeof key.x === "string";
|
|
200
|
+
const isP256 = key.kty === "EC" && key.crv === "P-256" && typeof key.x === "string" && typeof key.y === "string";
|
|
201
|
+
if (!isEd25519 && !isP256 || typeof key.d !== "string") {
|
|
202
|
+
throw new Error(`${filePath} must include Ed25519 or ECDSA P-256 private key material`);
|
|
203
|
+
}
|
|
204
|
+
const pub = {
|
|
205
|
+
kty: key.kty,
|
|
206
|
+
crv: key.crv,
|
|
207
|
+
x: key.x,
|
|
208
|
+
...isP256 ? { y: key.y } : {},
|
|
209
|
+
kid: typeof key.kid === "string" ? key.kid : ""
|
|
210
|
+
};
|
|
211
|
+
const alg = jwkSignatureAlg(pub);
|
|
212
|
+
const normalizedPub = { ...pub, alg, use: "sig", kid: pub.kid || await jwkThumbprint(pub) };
|
|
213
|
+
const normalized = { ...normalizedPub, d: key.d };
|
|
214
|
+
const probe = new TextEncoder().encode("dnsid-local-key-provider-self-check");
|
|
215
|
+
let verified = false;
|
|
216
|
+
try {
|
|
217
|
+
const signature = await crypto.subtle.sign(signAlgorithm(normalized), await importPrivateKey(normalized), probe);
|
|
218
|
+
verified = await verifyWithKey(probe, new Uint8Array(signature), normalizedPub, alg);
|
|
219
|
+
} catch {
|
|
220
|
+
verified = false;
|
|
221
|
+
}
|
|
222
|
+
if (!verified) throw new Error(`${filePath} has mismatched ${alg === "EdDSA" ? "Ed25519" : "ES256"} public/private key material`);
|
|
223
|
+
return normalized;
|
|
224
|
+
}
|
|
225
|
+
async function buildInitialStore(algorithm) {
|
|
226
|
+
return { active: await generateStoredKey(algorithm), retained: [], pending: [] };
|
|
227
|
+
}
|
|
228
|
+
async function generateStoredKey(algorithm) {
|
|
229
|
+
const generationAlgorithm = algorithm === "EdDSA" ? { name: "Ed25519" } : { name: "ECDSA", namedCurve: "P-256" };
|
|
230
|
+
const pair = await crypto.subtle.generateKey(generationAlgorithm, true, ["sign", "verify"]);
|
|
231
|
+
const priv = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
232
|
+
return validateAndNormalizePrivateJwk({ ...priv, kid: "" }, "generated key");
|
|
233
|
+
}
|
|
234
|
+
function toArrayBuffer(bytes) {
|
|
235
|
+
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
236
|
+
return bytes;
|
|
237
|
+
}
|
|
238
|
+
return new Uint8Array(bytes);
|
|
239
|
+
}
|
|
240
|
+
function localKeyAlgorithm(key) {
|
|
241
|
+
const alg = jwkSignatureAlg(key);
|
|
242
|
+
if (alg === "EdDSA" || alg === "ES256") return alg;
|
|
243
|
+
throw new ArgumentError(`unsupported local key algorithm: ${alg}`);
|
|
244
|
+
}
|
|
245
|
+
function signAlgorithm(key) {
|
|
246
|
+
return localKeyAlgorithm(key) === "EdDSA" ? { name: "Ed25519" } : { name: "ECDSA", hash: "SHA-256" };
|
|
247
|
+
}
|
|
248
|
+
async function importPrivateKey(key) {
|
|
249
|
+
const alg = localKeyAlgorithm(key);
|
|
250
|
+
return crypto.subtle.importKey(
|
|
251
|
+
"jwk",
|
|
252
|
+
{
|
|
253
|
+
kty: key.kty,
|
|
254
|
+
crv: key.crv,
|
|
255
|
+
x: key.x,
|
|
256
|
+
...alg === "ES256" ? { y: key.y } : {},
|
|
257
|
+
d: key.d,
|
|
258
|
+
use: "sig"
|
|
259
|
+
},
|
|
260
|
+
alg === "EdDSA" ? { name: "Ed25519" } : { name: "ECDSA", namedCurve: "P-256" },
|
|
261
|
+
false,
|
|
262
|
+
["sign"]
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/environment.ts
|
|
267
|
+
import { DNSSECMode } from "@dnsid-ai/protocol";
|
|
268
|
+
import { DEFAULT_REGISTRY_URL } from "@dnsid-ai/registry";
|
|
269
|
+
var dnsidEnvironmentVariables = {
|
|
270
|
+
domain: "DNSID_DOMAIN",
|
|
271
|
+
governanceId: "DNSID_GOVERNANCE_ID",
|
|
272
|
+
registryUrl: "DNSID_REGISTRY_URL",
|
|
273
|
+
apiKey: "DNSID_API_KEY",
|
|
274
|
+
statusUrl: "DNSID_STATUS_URL",
|
|
275
|
+
logRef: "DNSID_LOG_REF",
|
|
276
|
+
ekUrl: "DNSID_EK_URL",
|
|
277
|
+
kuUrl: "DNSID_KU_URL",
|
|
278
|
+
dnsServer: "DNSID_DNS_SERVER",
|
|
279
|
+
caBundlePath: "DNSID_CA_BUNDLE",
|
|
280
|
+
dnssecMode: "DNSID_DNSSEC_MODE",
|
|
281
|
+
publicUrl: "DNSID_PUBLIC_URL",
|
|
282
|
+
keyStorePath: "DNSID_KEY_STORE",
|
|
283
|
+
agentPort: "DNSID_AGENT_PORT",
|
|
284
|
+
agentName: "DNSID_AGENT_NAME"
|
|
285
|
+
};
|
|
286
|
+
var environmentSchema = {
|
|
287
|
+
domain: stringField(dnsidEnvironmentVariables.domain),
|
|
288
|
+
governanceId: stringField(dnsidEnvironmentVariables.governanceId),
|
|
289
|
+
registryUrl: stringField(dnsidEnvironmentVariables.registryUrl),
|
|
290
|
+
apiKey: stringField(dnsidEnvironmentVariables.apiKey),
|
|
291
|
+
statusUrl: stringField(dnsidEnvironmentVariables.statusUrl),
|
|
292
|
+
logRef: stringField(dnsidEnvironmentVariables.logRef),
|
|
293
|
+
ekUrl: stringField(dnsidEnvironmentVariables.ekUrl),
|
|
294
|
+
kuUrl: stringField(dnsidEnvironmentVariables.kuUrl),
|
|
295
|
+
dnsServer: stringField(dnsidEnvironmentVariables.dnsServer),
|
|
296
|
+
caBundlePath: stringField(dnsidEnvironmentVariables.caBundlePath),
|
|
297
|
+
dnssecMode: dnssecModeField(dnsidEnvironmentVariables.dnssecMode),
|
|
298
|
+
publicUrl: stringField(dnsidEnvironmentVariables.publicUrl),
|
|
299
|
+
keyStorePath: stringField(dnsidEnvironmentVariables.keyStorePath),
|
|
300
|
+
agentPort: positiveIntField(dnsidEnvironmentVariables.agentPort),
|
|
301
|
+
agentName: stringField(dnsidEnvironmentVariables.agentName)
|
|
302
|
+
};
|
|
303
|
+
function configFromEnvironment(envOrOptions = process.env, options = {}) {
|
|
304
|
+
const receivedOptions = isConfigFromEnvironmentOptions(envOrOptions);
|
|
305
|
+
const env = receivedOptions ? process.env : envOrOptions ?? process.env;
|
|
306
|
+
const resolvedOptions = receivedOptions ? envOrOptions : options;
|
|
307
|
+
const parsed = parseEnvironment(env);
|
|
308
|
+
requireFields(parsed, ["domain", "governanceId", ...resolvedOptions.require ?? []]);
|
|
309
|
+
const registryUrl = parsed.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
310
|
+
const statusUrl = parsed.statusUrl ?? protocolStatusUrl(registryUrl, parsed.domain);
|
|
311
|
+
const config = {
|
|
312
|
+
identity: defined({
|
|
313
|
+
domain: parsed.domain,
|
|
314
|
+
governanceId: parsed.governanceId,
|
|
315
|
+
logRef: parsed.logRef ?? "noop:0",
|
|
316
|
+
statusUrl,
|
|
317
|
+
ekUrl: parsed.ekUrl,
|
|
318
|
+
kuUrl: parsed.kuUrl
|
|
319
|
+
}),
|
|
320
|
+
verification: defined({ dnssecMode: parsed.dnssecMode }),
|
|
321
|
+
transport: defined({ dnsServer: parsed.dnsServer, caBundlePath: parsed.caBundlePath })
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
config,
|
|
325
|
+
registryUrl,
|
|
326
|
+
apiKey: parsed.apiKey,
|
|
327
|
+
publicUrl: parsed.publicUrl,
|
|
328
|
+
keyStorePath: parsed.keyStorePath,
|
|
329
|
+
agentName: parsed.agentName,
|
|
330
|
+
agentPort: parsed.agentPort
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function registryClientOptionsFromEnvironment(env = process.env) {
|
|
334
|
+
const parsed = parseEnvironment(env);
|
|
335
|
+
return defined({ baseUrl: parsed.registryUrl ?? DEFAULT_REGISTRY_URL, token: parsed.apiKey });
|
|
336
|
+
}
|
|
337
|
+
function defined(value) {
|
|
338
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
339
|
+
}
|
|
340
|
+
function isConfigFromEnvironmentOptions(value) {
|
|
341
|
+
if (value === void 0) return false;
|
|
342
|
+
const keys = Object.keys(value);
|
|
343
|
+
if (keys.length === 0) return true;
|
|
344
|
+
return keys.every((key) => key === "require") && Array.isArray(value.require);
|
|
345
|
+
}
|
|
346
|
+
function parseEnvironment(env) {
|
|
347
|
+
const parsed = {};
|
|
348
|
+
for (const fieldName of Object.keys(environmentSchema)) {
|
|
349
|
+
const value = readEnvironmentField(env, fieldName);
|
|
350
|
+
if (value !== void 0) parsed[fieldName] = value;
|
|
351
|
+
}
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
354
|
+
function readEnvironmentField(env, fieldName) {
|
|
355
|
+
const field = environmentSchema[fieldName];
|
|
356
|
+
const raw = env[field.variable];
|
|
357
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
358
|
+
return field.parse(raw, field.variable);
|
|
359
|
+
}
|
|
360
|
+
function requireFields(parsed, fields) {
|
|
361
|
+
for (const field of fields) {
|
|
362
|
+
if (parsed[field] === void 0) throw new Error(`${environmentSchema[field].variable} is required`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function stringField(variable) {
|
|
366
|
+
return { variable, parse: (value) => value };
|
|
367
|
+
}
|
|
368
|
+
function positiveIntField(variable) {
|
|
369
|
+
return {
|
|
370
|
+
variable,
|
|
371
|
+
parse(value, name) {
|
|
372
|
+
const parsed = Number(value);
|
|
373
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
374
|
+
throw new Error(`${name} must be a positive integer`);
|
|
375
|
+
}
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function dnssecModeField(variable) {
|
|
381
|
+
return {
|
|
382
|
+
variable,
|
|
383
|
+
parse(value, name) {
|
|
384
|
+
if (!Object.values(DNSSECMode).includes(value)) {
|
|
385
|
+
throw new Error(`invalid ${name} "${value}"; expected one of: ${Object.values(DNSSECMode).join(", ")}`);
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function protocolStatusUrl(registryUrl, domain) {
|
|
392
|
+
return `${registryUrl.replace(/\/$/, "")}/v1/status/${encodeURIComponent(domain)}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/node.ts
|
|
396
|
+
async function createNodeIdentityManager(config, deps = {}) {
|
|
397
|
+
const { transport, ...core } = validateDnsidConfig(config);
|
|
398
|
+
const { dnsServer, caBundlePath, allowedUnsafeHosts } = transport;
|
|
399
|
+
if (dnsServer !== void 0 && deps.dnsResolver && deps.fetchJson) {
|
|
400
|
+
throw new ArgumentError2("config.transport.dnsServer has no SDK-managed consumer when both dnsResolver and fetchJson are injected");
|
|
401
|
+
}
|
|
402
|
+
if (caBundlePath !== void 0 && deps.fetchJson) {
|
|
403
|
+
throw new ArgumentError2("config.transport.caBundlePath has no SDK-managed consumer when fetchJson is injected");
|
|
404
|
+
}
|
|
405
|
+
if (allowedUnsafeHosts !== void 0 && deps.fetchJson) {
|
|
406
|
+
throw new ArgumentError2("config.transport.allowedUnsafeHosts has no SDK-managed consumer when fetchJson is injected");
|
|
407
|
+
}
|
|
408
|
+
const transportModule = deps.fetchJson && deps.dnsResolver ? null : await loadNodeTransport();
|
|
409
|
+
const fetchJson = deps.fetchJson ?? ((url, fetchOptions) => transportModule.fetchJson(url, { ...fetchOptions, dnsServer, caBundlePath, allowedUnsafeHosts }));
|
|
410
|
+
const dnsResolver = deps.dnsResolver ?? transportModule.createDefaultDnsResolver({ dnsServer });
|
|
411
|
+
return new IdentityManager(core, { ...deps, dnsResolver, fetchJson });
|
|
412
|
+
}
|
|
413
|
+
async function createNodeIdentityVerifier(config = {}, deps = {}) {
|
|
414
|
+
return createNodeIdentityManager(config, deps);
|
|
415
|
+
}
|
|
416
|
+
async function createNodeIdentityManagerFromDnsid(options = {}, deps = {}) {
|
|
417
|
+
const dnsidDir = options.dnsidDir ?? options.env?.["DNSID_CONFIG_DIR"] ?? process.env["DNSID_CONFIG_DIR"] ?? path2.join(os2.homedir(), ".dnsid");
|
|
418
|
+
let configPath = path2.join(dnsidDir, "config.json");
|
|
419
|
+
let raw = await readConfig(configPath);
|
|
420
|
+
const pointerDomain = stringField2(raw, "domain", "fqdn");
|
|
421
|
+
if (pointerDomain) {
|
|
422
|
+
const identityConfigPath = path2.join(dnsidDir, normalizeFQDN2(pointerDomain, true), "config.json");
|
|
423
|
+
try {
|
|
424
|
+
raw = await readJson(identityConfigPath);
|
|
425
|
+
configPath = identityConfigPath;
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (e.code !== "ENOENT") throw e;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const field = (...keys) => stringField2(raw, ...keys);
|
|
431
|
+
const loaded = {
|
|
432
|
+
domain: field("domain", "fqdn"),
|
|
433
|
+
governanceId: field("governanceId", "governance_id"),
|
|
434
|
+
logRef: field("logRef", "log_ref"),
|
|
435
|
+
statusUrl: field("statusUrl", "status_url"),
|
|
436
|
+
ekUrl: field("ekUrl", "ek_url"),
|
|
437
|
+
kuUrl: field("kuUrl", "ku_url"),
|
|
438
|
+
capabilitiesUrl: field("capabilitiesUrl", "capabilities_url"),
|
|
439
|
+
publishProfile: field("publishProfile", "publish_profile"),
|
|
440
|
+
maxKeyAge: maxKeyAgeValue(raw.maxKeyAge ?? raw.max_key_age, configPath)
|
|
441
|
+
};
|
|
442
|
+
const identity = { ...definedOnly(loaded), ...definedOnly(options.config?.identity ?? {}) };
|
|
443
|
+
if (!identity.domain) throw new Error(`${configPath} must include domain`);
|
|
444
|
+
if (!identity.governanceId) throw new Error(`${configPath} must include governanceId or governance_id`);
|
|
445
|
+
identity.logRef ??= "noop:0";
|
|
446
|
+
identity.statusUrl ??= protocolStatusUrl2(field("registryUrl", "registry_url", "server_url") ?? DEFAULT_REGISTRY_URL2, identity.domain);
|
|
447
|
+
const keyProvider = deps.keyProvider ?? await LocalKeyProvider.fromDirectory(await identityKeyDir(dnsidDir, identity.domain));
|
|
448
|
+
const entityKeyProvider = deps.entityKeyProvider ?? await optionalEntityKeyProvider(
|
|
449
|
+
path2.dirname(configPath),
|
|
450
|
+
field("entityKeyPath", "entity_key_path"),
|
|
451
|
+
field("entityKeyDir", "entity_key_dir")
|
|
452
|
+
);
|
|
453
|
+
return createNodeIdentityManager(
|
|
454
|
+
{ identity, verification: options.config?.verification, transport: options.config?.transport },
|
|
455
|
+
{ ...deps, keyProvider, entityKeyProvider }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
function definedOnly(value) {
|
|
459
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
460
|
+
}
|
|
461
|
+
async function readConfig(configPath) {
|
|
462
|
+
return readJson(configPath).catch((e) => {
|
|
463
|
+
if (e.code === "ENOENT") {
|
|
464
|
+
throw new Error(`${configPath} not found; run the DNSid CLI to register your domain first`);
|
|
465
|
+
}
|
|
466
|
+
throw e;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async function optionalEntityKeyProvider(root, file, legacyDir) {
|
|
470
|
+
if (file) {
|
|
471
|
+
return LocalKeyProvider.fromFile(path2.isAbsolute(file) ? file : path2.resolve(root, file));
|
|
472
|
+
}
|
|
473
|
+
if (!legacyDir) return void 0;
|
|
474
|
+
return LocalKeyProvider.fromDirectory(path2.isAbsolute(legacyDir) ? legacyDir : path2.resolve(root, legacyDir));
|
|
475
|
+
}
|
|
476
|
+
async function identityKeyDir(dnsidDir, domain) {
|
|
477
|
+
for (const filename of ["private.jwk", "private.pem"]) {
|
|
478
|
+
try {
|
|
479
|
+
await fs3.access(path2.join(dnsidDir, filename));
|
|
480
|
+
return dnsidDir;
|
|
481
|
+
} catch (e) {
|
|
482
|
+
if (e.code !== "ENOENT") throw e;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return path2.join(dnsidDir, normalizeFQDN2(domain, true));
|
|
486
|
+
}
|
|
487
|
+
function stringField2(raw, ...keys) {
|
|
488
|
+
return keys.map((key) => raw[key]).find((value) => typeof value === "string" && value !== "");
|
|
489
|
+
}
|
|
490
|
+
function maxKeyAgeValue(value, configPath) {
|
|
491
|
+
if (value == null || value === "") return void 0;
|
|
492
|
+
if (["24h", "7d", "30d", "90d"].includes(String(value))) return value;
|
|
493
|
+
throw new Error(`${configPath} has invalid maxKeyAge "${String(value)}"`);
|
|
494
|
+
}
|
|
495
|
+
function protocolStatusUrl2(registryUrl, domain) {
|
|
496
|
+
return `${registryUrl.replace(/\/$/, "")}/v1/status/${encodeURIComponent(normalizeFQDN2(domain, true))}`;
|
|
497
|
+
}
|
|
498
|
+
async function loadNodeTransport() {
|
|
499
|
+
try {
|
|
500
|
+
return await import("@dnsid-ai/transport");
|
|
501
|
+
} catch (e) {
|
|
502
|
+
const err = e;
|
|
503
|
+
if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
|
|
504
|
+
throw new Error("`@dnsid-ai/sdk/node` HTTPS defaults require optional peer `@dnsid-ai/transport`; install it or inject fetchJson.");
|
|
505
|
+
}
|
|
506
|
+
throw e;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
export {
|
|
510
|
+
LocalKeyProvider,
|
|
511
|
+
configFromEnvironment,
|
|
512
|
+
createNodeIdentityManager,
|
|
513
|
+
createNodeIdentityManagerFromDnsid,
|
|
514
|
+
createNodeIdentityVerifier,
|
|
515
|
+
dnsidEnvironmentVariables,
|
|
516
|
+
keyStorePathFromEnvironment,
|
|
517
|
+
registryClientOptionsFromEnvironment
|
|
518
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dnsid-ai/sdk",
|
|
3
|
+
"version": "0.20.1",
|
|
4
|
+
"description": "Runtime-neutral DNSid SDK entrypoint with optional Node.js convenience helpers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./node": {
|
|
14
|
+
"types": "./dist/node.d.ts",
|
|
15
|
+
"import": "./dist/node.js",
|
|
16
|
+
"require": "./dist/node.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/dnsid-ai/dnsid-ts.git",
|
|
25
|
+
"directory": "packages/sdk"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/dnsid-ai/dnsid-ts/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/dnsid-ai/dnsid-ts#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"dnsid",
|
|
33
|
+
"sdk",
|
|
34
|
+
"dns",
|
|
35
|
+
"identity"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/index.ts src/node.ts --format esm,cjs --dts --clean",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@dnsid-ai/protocol": "^0.20.1",
|
|
43
|
+
"@dnsid-ai/http-signatures": "^0.20.1",
|
|
44
|
+
"@dnsid-ai/jose": "^0.20.1",
|
|
45
|
+
"@dnsid-ai/log-c2sp-tlog": "^0.20.1",
|
|
46
|
+
"@dnsid-ai/registry": "^0.20.1",
|
|
47
|
+
"@dnsid-ai/web-bot-auth": "^0.20.1"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@dnsid-ai/transport": "^0.20.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@dnsid-ai/transport": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"license": "Apache-2.0"
|
|
61
|
+
}
|