@anchor-secrets/sdk 0.0.0-stage → 0.4.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/dist/index.cjs ADDED
@@ -0,0 +1,555 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AnchorError: () => AnchorError,
24
+ createAnchorClient: () => createAnchorClient,
25
+ memoryStore: () => memoryStore
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/cache.ts
30
+ var import_node_crypto = require("node:crypto");
31
+
32
+ // src/errors.ts
33
+ var AnchorError = class extends Error {
34
+ status;
35
+ code;
36
+ /** Whether repeating the request may succeed (network, timeout, 429, 502, 503, 504). */
37
+ retryable;
38
+ /** The server's `Retry-After`, in milliseconds, when it sent one. */
39
+ retryAfterMs;
40
+ constructor(status, code, message, options = {}) {
41
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
42
+ this.name = "AnchorError";
43
+ this.status = status;
44
+ this.code = code;
45
+ this.retryable = options.retryable ?? false;
46
+ if (options.retryAfterMs !== void 0) this.retryAfterMs = options.retryAfterMs;
47
+ }
48
+ };
49
+ var abortedError = () => new AnchorError(0, "aborted", "The request to Anchor was cancelled");
50
+
51
+ // src/cache.ts
52
+ function memoryStore() {
53
+ const entries = /* @__PURE__ */ new Map();
54
+ return {
55
+ get: (key) => entries.get(key),
56
+ set: (key, entry) => {
57
+ entries.set(key, entry);
58
+ }
59
+ };
60
+ }
61
+ function cacheKey(origin, token, q) {
62
+ const fingerprint = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, 16);
63
+ return JSON.stringify([
64
+ origin,
65
+ fingerprint,
66
+ q.project,
67
+ q.environment,
68
+ q.path,
69
+ q.expand,
70
+ q.includeImports
71
+ ]);
72
+ }
73
+ function untilAborted(promise, signal) {
74
+ if (!signal) return promise;
75
+ if (signal.aborted) return Promise.reject(abortedError());
76
+ return new Promise((resolve, reject) => {
77
+ const onAbort = () => reject(abortedError());
78
+ signal.addEventListener("abort", onAbort, { once: true });
79
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
80
+ });
81
+ }
82
+ var SecretCache = class {
83
+ constructor(policy, emit, now = () => Date.now()) {
84
+ this.policy = policy;
85
+ this.emit = emit;
86
+ this.now = now;
87
+ }
88
+ policy;
89
+ emit;
90
+ now;
91
+ inflight = /* @__PURE__ */ new Map();
92
+ /** When the last fetch of a key failed; the stale entry is served without asking for one ttl. */
93
+ failedAt = /* @__PURE__ */ new Map();
94
+ /**
95
+ * The folder's entry. With `strict`, a failed fetch rejects instead of falling back, so a
96
+ * caller that asked for new values learns it did not get them.
97
+ */
98
+ async load(key, fetch, opts = {}) {
99
+ if (opts.signal?.aborted) throw abortedError();
100
+ const entry = await this.read(key);
101
+ if (entry && !opts.fresh && this.servable(key, entry)) return entry;
102
+ const flight = this.inflight.get(key) ?? this.start(key, fetch, entry);
103
+ flight.waiters++;
104
+ let outcome;
105
+ try {
106
+ outcome = await untilAborted(flight.promise, opts.signal);
107
+ } catch (error) {
108
+ if (opts.signal?.aborted && --flight.waiters === 0) {
109
+ flight.controller.abort();
110
+ if (this.inflight.get(key) === flight) this.inflight.delete(key);
111
+ }
112
+ throw error;
113
+ }
114
+ if (opts.strict && outcome.error) throw outcome.error;
115
+ return outcome.entry;
116
+ }
117
+ start(key, fetch, stale) {
118
+ const controller = new AbortController();
119
+ const promise = this.fetch(
120
+ key,
121
+ () => fetch(controller.signal),
122
+ stale,
123
+ controller.signal
124
+ ).finally(() => {
125
+ if (this.inflight.get(key)?.promise === promise) this.inflight.delete(key);
126
+ });
127
+ const flight = { promise, controller, waiters: 0 };
128
+ this.inflight.set(key, flight);
129
+ return flight;
130
+ }
131
+ /** A store that fails is a miss: Anchor stays reachable when Redis (or a file) is not. */
132
+ async read(key) {
133
+ try {
134
+ return await this.policy.store.get(key);
135
+ } catch (error) {
136
+ this.emit({ type: "store_failed", operation: "get", error });
137
+ return void 0;
138
+ }
139
+ }
140
+ async write(key, entry) {
141
+ try {
142
+ await this.policy.store.set(key, entry);
143
+ } catch (error) {
144
+ this.emit({ type: "store_failed", operation: "set", error });
145
+ }
146
+ }
147
+ servable(key, entry) {
148
+ const now = this.now();
149
+ if (now - entry.fetchedAt < this.policy.ttlMs) return true;
150
+ const failed = this.failedAt.get(key);
151
+ return failed !== void 0 && now - failed < this.policy.ttlMs && this.usable(entry, now);
152
+ }
153
+ usable(entry, now) {
154
+ return now - entry.fetchedAt <= this.policy.ttlMs + this.policy.maxStaleMs;
155
+ }
156
+ async fetch(key, fetch, stale, signal) {
157
+ try {
158
+ const entry = { secrets: await fetch(), fetchedAt: this.now() };
159
+ await this.write(key, entry);
160
+ this.failedAt.delete(key);
161
+ return { entry };
162
+ } catch (error) {
163
+ if (signal.aborted) throw error;
164
+ const now = this.now();
165
+ if (!(error instanceof AnchorError) || !stale || !this.usable(stale, now)) throw error;
166
+ this.failedAt.set(key, now);
167
+ const staleForMs = Math.max(0, now - stale.fetchedAt - this.policy.ttlMs);
168
+ this.emit({ type: "refresh_failed", error, staleForMs });
169
+ return { entry: stale, error };
170
+ }
171
+ }
172
+ };
173
+
174
+ // src/retry.ts
175
+ var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 250, maxDelayMs: 5e3 };
176
+ function delayFor(error, attempt, policy) {
177
+ if (error.retryAfterMs !== void 0) return Math.min(error.retryAfterMs, policy.maxDelayMs);
178
+ const cap = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (attempt - 1));
179
+ return Math.floor(Math.random() * cap);
180
+ }
181
+ function wait(ms, signal) {
182
+ return new Promise((resolve, reject) => {
183
+ const onAbort = () => {
184
+ clearTimeout(timer);
185
+ reject(abortedError());
186
+ };
187
+ const timer = setTimeout(() => {
188
+ signal?.removeEventListener("abort", onAbort);
189
+ resolve();
190
+ }, ms);
191
+ signal?.addEventListener("abort", onAbort, { once: true });
192
+ });
193
+ }
194
+ async function withRetry(run, policy, opts) {
195
+ const attempts = policy ? Math.max(1, policy.attempts) : 1;
196
+ for (let attempt = 1; ; attempt++) {
197
+ if (opts.signal?.aborted) throw abortedError();
198
+ try {
199
+ return await run();
200
+ } catch (error) {
201
+ if (!policy || !(error instanceof AnchorError) || !error.retryable || attempt >= attempts) {
202
+ throw error;
203
+ }
204
+ const delayMs = delayFor(error, attempt, policy);
205
+ opts.emit({ type: "retry", attempt, delayMs, error });
206
+ await wait(delayMs, opts.signal);
207
+ }
208
+ }
209
+ }
210
+
211
+ // src/config.ts
212
+ var DEFAULT_TIMEOUT_MS = 1e4;
213
+ function fromEnv(name) {
214
+ const value = typeof process === "undefined" ? void 0 : process.env[name];
215
+ return value === "" ? void 0 : value;
216
+ }
217
+ function resolveConfig(opts) {
218
+ const apiUrl = opts.apiUrl || fromEnv("ANCHOR_API_URL");
219
+ if (!apiUrl) {
220
+ throw new AnchorError(
221
+ 0,
222
+ "config",
223
+ "Anchor API URL is not set: pass apiUrl or set ANCHOR_API_URL"
224
+ );
225
+ }
226
+ let origin;
227
+ try {
228
+ origin = new URL(apiUrl).origin;
229
+ } catch {
230
+ throw new AnchorError(
231
+ 0,
232
+ "config",
233
+ "Anchor API URL is not a valid URL (apiUrl or ANCHOR_API_URL)"
234
+ );
235
+ }
236
+ const token = opts.token || fromEnv("ANCHOR_TOKEN");
237
+ if (!token) {
238
+ throw new AnchorError(0, "config", "Anchor token is not set: pass token or set ANCHOR_TOKEN");
239
+ }
240
+ const f = opts.fetch ?? globalThis.fetch;
241
+ if (typeof f !== "function") {
242
+ throw new AnchorError(
243
+ 0,
244
+ "config",
245
+ "No fetch implementation: use Node 20 or later, or pass fetch"
246
+ );
247
+ }
248
+ return {
249
+ base: apiUrl.replace(/\/+$/, ""),
250
+ origin,
251
+ token,
252
+ fetch: f,
253
+ timeoutMs: timeoutOption(opts.timeoutMs),
254
+ retry: retryPolicy(opts.retry),
255
+ onEvent: opts.onEvent
256
+ };
257
+ }
258
+ function numberOption(name, value, fallback, rule) {
259
+ if (value === void 0) return fallback;
260
+ const valid = typeof value === "number" && !Number.isNaN(value) && (rule.infinite || Number.isFinite(value)) && value >= rule.min && (!rule.integer || Number.isInteger(value));
261
+ if (!valid) {
262
+ const kind = rule.integer ? "an integer" : "a number";
263
+ throw new AnchorError(
264
+ 0,
265
+ "config",
266
+ `Anchor option ${name} must be ${kind} of at least ${rule.min}`
267
+ );
268
+ }
269
+ return value;
270
+ }
271
+ var timeoutOption = (value) => numberOption("timeoutMs", value, DEFAULT_TIMEOUT_MS, { min: 1 });
272
+ function retryPolicy(retry) {
273
+ if (retry === false) return false;
274
+ return {
275
+ attempts: numberOption("retry.attempts", retry?.attempts, DEFAULT_RETRY.attempts, {
276
+ min: 1,
277
+ integer: true
278
+ }),
279
+ baseDelayMs: numberOption("retry.baseDelayMs", retry?.baseDelayMs, DEFAULT_RETRY.baseDelayMs, {
280
+ min: 0
281
+ }),
282
+ maxDelayMs: numberOption("retry.maxDelayMs", retry?.maxDelayMs, DEFAULT_RETRY.maxDelayMs, {
283
+ min: 0
284
+ })
285
+ };
286
+ }
287
+ function validateOptions(opts) {
288
+ timeoutOption(opts.timeoutMs);
289
+ retryPolicy(opts.retry);
290
+ }
291
+ function emitSafely(onEvent, event) {
292
+ try {
293
+ onEvent?.(event);
294
+ } catch {
295
+ }
296
+ }
297
+ var DEFAULT_TTL_MS = 6e4;
298
+ function cachePolicy(cache) {
299
+ if (cache === false) return false;
300
+ return {
301
+ ttlMs: numberOption("cache.ttlMs", cache?.ttlMs, DEFAULT_TTL_MS, { min: 0 }),
302
+ maxStaleMs: numberOption("cache.maxStaleMs", cache?.maxStaleMs, Number.POSITIVE_INFINITY, {
303
+ min: 0,
304
+ infinite: true
305
+ }),
306
+ store: cache?.store ?? memoryStore()
307
+ };
308
+ }
309
+
310
+ // ../shared/src/value-decode.ts
311
+ function decodeTypedValue(type, value) {
312
+ switch (type) {
313
+ case "json":
314
+ case "list":
315
+ return JSON.parse(value);
316
+ case "number":
317
+ return Number(value);
318
+ case "boolean":
319
+ return value === "true";
320
+ default:
321
+ return value;
322
+ }
323
+ }
324
+
325
+ // src/decode.ts
326
+ function toSecret(s) {
327
+ if (typeof s.value !== "string") {
328
+ throw new AnchorError(200, "bad_response", `Anchor returned ${s.key} without a value`);
329
+ }
330
+ const out = {
331
+ key: s.key,
332
+ path: s.path,
333
+ type: s.type,
334
+ sensitive: s.sensitive,
335
+ version: s.version,
336
+ value: s.value
337
+ };
338
+ if (s.importedFrom) out.importedFrom = { ...s.importedFrom };
339
+ return out;
340
+ }
341
+ function decode(s) {
342
+ try {
343
+ return decodeTypedValue(s.type, s.value);
344
+ } catch {
345
+ throw new AnchorError(200, "bad_response", `Secret ${s.key} is not valid ${s.type}`);
346
+ }
347
+ }
348
+ function decodeAll(secrets) {
349
+ const entries = /* @__PURE__ */ new Map();
350
+ for (const s of secrets) {
351
+ if (!entries.has(s.key)) entries.set(s.key, decode(s));
352
+ }
353
+ return Object.fromEntries(entries);
354
+ }
355
+
356
+ // src/folder.ts
357
+ var snapshotOf = (entry) => ({
358
+ entry,
359
+ values: Object.freeze(decodeAll(entry.secrets))
360
+ });
361
+ async function openFolder(core, query, options = {}) {
362
+ let snapshot = snapshotOf(await core.loadEntry(query, { signal: options.signal }));
363
+ let checkedAt = Date.now();
364
+ let failed = false;
365
+ let pending;
366
+ const accept = (entry) => {
367
+ failed = false;
368
+ if (entry !== snapshot.entry) snapshot = snapshotOf(entry);
369
+ };
370
+ const refreshInBackground = () => {
371
+ pending = core.loadEntry(query).then(accept, (error) => {
372
+ failed = true;
373
+ if (error instanceof AnchorError) {
374
+ const staleForMs = Math.max(0, Date.now() - snapshot.entry.fetchedAt - core.ttlMs);
375
+ core.emit({ type: "refresh_failed", error, staleForMs });
376
+ }
377
+ }).finally(() => {
378
+ checkedAt = Date.now();
379
+ pending = void 0;
380
+ });
381
+ };
382
+ const values = () => {
383
+ if (!pending && Date.now() - checkedAt >= core.ttlMs) refreshInBackground();
384
+ if (failed && Date.now() - snapshot.entry.fetchedAt > core.ttlMs + core.maxStaleMs) {
385
+ throw new AnchorError(
386
+ 0,
387
+ "stale",
388
+ "Anchor values are older than ttlMs + maxStaleMs and refreshing them failed"
389
+ );
390
+ }
391
+ return snapshot.values;
392
+ };
393
+ const has = (key) => Object.hasOwn(values(), key);
394
+ return {
395
+ get: (key) => has(key) ? snapshot.values[key] : void 0,
396
+ getOrThrow(key) {
397
+ if (!has(key)) throw new Error(`Anchor secret ${key} is not set`);
398
+ return snapshot.values[key];
399
+ },
400
+ has,
401
+ keys: () => Object.keys(values()),
402
+ toObject: values,
403
+ async refresh() {
404
+ try {
405
+ accept(await core.loadEntry(query, { fresh: true, strict: true }));
406
+ } finally {
407
+ checkedAt = Date.now();
408
+ }
409
+ }
410
+ };
411
+ }
412
+
413
+ // src/transport.ts
414
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
415
+ function retryAfterMs(res) {
416
+ if (res.status !== 429 && res.status !== 503) return void 0;
417
+ const header = res.headers.get("retry-after")?.trim();
418
+ if (!header || !/^\d+$/.test(header)) return void 0;
419
+ return Number(header) * 1e3;
420
+ }
421
+ async function errorFromResponse(res) {
422
+ let body;
423
+ try {
424
+ body = await res.json();
425
+ } catch {
426
+ body = void 0;
427
+ }
428
+ const options = {
429
+ retryable: RETRYABLE_STATUSES.has(res.status),
430
+ retryAfterMs: retryAfterMs(res)
431
+ };
432
+ const error = body?.error;
433
+ if (typeof error?.code === "string" && typeof error.message === "string") {
434
+ return new AnchorError(res.status, error.code, error.message, options);
435
+ }
436
+ return new AnchorError(res.status, "http_error", `Anchor answered HTTP ${res.status}`, options);
437
+ }
438
+ async function fetchJson(cfg, url, signal) {
439
+ if (signal?.aborted) throw abortedError();
440
+ const controller = new AbortController();
441
+ let timedOut = false;
442
+ const timer = setTimeout(() => {
443
+ timedOut = true;
444
+ controller.abort();
445
+ }, cfg.timeoutMs);
446
+ const onAbort = () => controller.abort();
447
+ signal?.addEventListener("abort", onAbort, { once: true });
448
+ try {
449
+ const res = await cfg.fetch(url, {
450
+ method: "GET",
451
+ headers: { authorization: `Bearer ${cfg.token}`, accept: "application/json" },
452
+ signal: controller.signal
453
+ });
454
+ if (!res.ok) throw await errorFromResponse(res);
455
+ try {
456
+ return await res.json();
457
+ } catch (cause) {
458
+ if (controller.signal.aborted) throw cause;
459
+ throw new AnchorError(
460
+ res.status,
461
+ "bad_response",
462
+ "Anchor returned a response that is not JSON"
463
+ );
464
+ }
465
+ } catch (cause) {
466
+ if (cause instanceof AnchorError) throw cause;
467
+ if (timedOut) {
468
+ throw new AnchorError(
469
+ 0,
470
+ "timeout",
471
+ `Anchor at ${cfg.origin} did not answer within ${cfg.timeoutMs} ms`,
472
+ { retryable: true }
473
+ );
474
+ }
475
+ if (signal?.aborted) throw abortedError();
476
+ const reason = cause instanceof Error ? cause.message : "request failed";
477
+ throw new AnchorError(0, "network", `Could not reach Anchor at ${cfg.origin}: ${reason}`, {
478
+ cause,
479
+ retryable: true
480
+ });
481
+ } finally {
482
+ clearTimeout(timer);
483
+ signal?.removeEventListener("abort", onAbort);
484
+ }
485
+ }
486
+
487
+ // src/client.ts
488
+ function normalize(q) {
489
+ return {
490
+ project: q.project,
491
+ environment: q.environment,
492
+ path: q.path ?? "/",
493
+ expand: q.expand ?? true,
494
+ includeImports: q.includeImports ?? true
495
+ };
496
+ }
497
+ async function fetchFolder(cfg, q, emit, signal) {
498
+ const params = new URLSearchParams({
499
+ path: q.path,
500
+ reveal: "true",
501
+ expand: String(q.expand),
502
+ includeImports: String(q.includeImports)
503
+ });
504
+ const url = `${cfg.base}/api/v1/projects/${encodeURIComponent(q.project)}/environments/${encodeURIComponent(q.environment)}/secrets?${params}`;
505
+ const body = await withRetry(() => fetchJson(cfg, url, signal), cfg.retry, {
506
+ signal,
507
+ emit
508
+ });
509
+ if (!body || !Array.isArray(body.secrets)) {
510
+ throw new AnchorError(200, "bad_response", "Anchor returned an unexpected response");
511
+ }
512
+ return body.secrets.map(toSecret);
513
+ }
514
+ var copy = (s) => s.importedFrom ? { ...s, importedFrom: { ...s.importedFrom } } : { ...s };
515
+ function createClientCore(options) {
516
+ validateOptions(options);
517
+ const emit = (event) => emitSafely(options.onEvent, event);
518
+ const policy = cachePolicy(options.cache);
519
+ const cache = policy ? new SecretCache(policy, emit) : void 0;
520
+ return {
521
+ ttlMs: policy ? policy.ttlMs : DEFAULT_TTL_MS,
522
+ maxStaleMs: policy ? policy.maxStaleMs : Number.POSITIVE_INFINITY,
523
+ emit,
524
+ async loadEntry(q, o = {}) {
525
+ const cfg = resolveConfig(options);
526
+ const query = normalize(q);
527
+ if (!cache) {
528
+ return { secrets: await fetchFolder(cfg, query, emit, o.signal), fetchedAt: Date.now() };
529
+ }
530
+ return cache.load(
531
+ cacheKey(cfg.origin, cfg.token, query),
532
+ (signal) => fetchFolder(cfg, query, emit, signal),
533
+ o
534
+ );
535
+ }
536
+ };
537
+ }
538
+ function createAnchorClient(options = {}) {
539
+ const core = createClientCore(options);
540
+ return {
541
+ async listSecrets(q, o) {
542
+ return (await core.loadEntry(q, o)).secrets.map(copy);
543
+ },
544
+ async loadSecrets(q, o) {
545
+ return decodeAll((await core.loadEntry(q, o)).secrets);
546
+ },
547
+ folder: (q, o) => openFolder(core, q, o)
548
+ };
549
+ }
550
+ // Annotate the CommonJS export names for ESM import in node:
551
+ 0 && (module.exports = {
552
+ AnchorError,
553
+ createAnchorClient,
554
+ memoryStore
555
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @anchor-secrets/sdk: a Node client that reads secrets from an Anchor server
3
+ * (spec 2026-11-07-v0.4.0-sdk-nestjs). This file only re-exports the public API.
4
+ */
5
+ export { memoryStore } from "./cache.cjs";
6
+ export { createAnchorClient } from "./client.cjs";
7
+ export { AnchorError } from "./errors.cjs";
8
+ export type { AnchorClient, AnchorEvent, AnchorFolder, AnchorSecret, AnchorValue, AnchorValueType, CacheEntry, CacheOptions, CacheStore, ClientOptions, ListQuery, RequestOptions, RetryOptions, } from "./types.cjs";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @anchor-secrets/sdk: a Node client that reads secrets from an Anchor server
3
+ * (spec 2026-11-07-v0.4.0-sdk-nestjs). This file only re-exports the public API.
4
+ */
5
+ export { memoryStore } from "./cache.js";
6
+ export { createAnchorClient } from "./client.js";
7
+ export { AnchorError } from "./errors.js";
8
+ export type { AnchorClient, AnchorEvent, AnchorFolder, AnchorSecret, AnchorValue, AnchorValueType, CacheEntry, CacheOptions, CacheStore, ClientOptions, ListQuery, RequestOptions, RetryOptions, } from "./types.js";