@barekey/sdk 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.
Files changed (57) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +21 -0
  3. package/dist/client.d.ts +41 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +302 -0
  6. package/dist/errors.d.ts +461 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +343 -0
  9. package/dist/handle.d.ts +20 -0
  10. package/dist/handle.d.ts.map +1 -0
  11. package/dist/handle.js +35 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +3 -0
  15. package/dist/internal/cache.d.ts +13 -0
  16. package/dist/internal/cache.d.ts.map +1 -0
  17. package/dist/internal/cache.js +24 -0
  18. package/dist/internal/evaluate.d.ts +7 -0
  19. package/dist/internal/evaluate.d.ts.map +1 -0
  20. package/dist/internal/evaluate.js +176 -0
  21. package/dist/internal/http.d.ts +19 -0
  22. package/dist/internal/http.d.ts.map +1 -0
  23. package/dist/internal/http.js +92 -0
  24. package/dist/internal/node-runtime.d.ts +19 -0
  25. package/dist/internal/node-runtime.d.ts.map +1 -0
  26. package/dist/internal/node-runtime.js +422 -0
  27. package/dist/internal/requirements.d.ts +3 -0
  28. package/dist/internal/requirements.d.ts.map +1 -0
  29. package/dist/internal/requirements.js +40 -0
  30. package/dist/internal/runtime.d.ts +15 -0
  31. package/dist/internal/runtime.d.ts.map +1 -0
  32. package/dist/internal/runtime.js +135 -0
  33. package/dist/internal/ttl.d.ts +4 -0
  34. package/dist/internal/ttl.d.ts.map +1 -0
  35. package/dist/internal/ttl.js +30 -0
  36. package/dist/internal/typegen.d.ts +25 -0
  37. package/dist/internal/typegen.d.ts.map +1 -0
  38. package/dist/internal/typegen.js +75 -0
  39. package/dist/types.d.ts +130 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +1 -0
  42. package/generated.d.ts +16 -0
  43. package/index.d.ts +2 -0
  44. package/package.json +42 -0
  45. package/src/client.ts +422 -0
  46. package/src/errors.ts +420 -0
  47. package/src/handle.ts +67 -0
  48. package/src/index.ts +60 -0
  49. package/src/internal/cache.ts +33 -0
  50. package/src/internal/evaluate.ts +232 -0
  51. package/src/internal/http.ts +134 -0
  52. package/src/internal/node-runtime.ts +581 -0
  53. package/src/internal/requirements.ts +57 -0
  54. package/src/internal/runtime.ts +199 -0
  55. package/src/internal/ttl.ts +41 -0
  56. package/src/internal/typegen.ts +124 -0
  57. package/src/types.ts +189 -0
package/src/client.ts ADDED
@@ -0,0 +1,422 @@
1
+ import {
2
+ BillingUnavailableError,
3
+ FsNotAvailableError,
4
+ NetworkError,
5
+ VariableNotFoundError,
6
+ } from "./errors.js";
7
+ import { BarekeyEnvHandle } from "./handle.js";
8
+ import {
9
+ evaluateDefinition,
10
+ inferSelectedArmFromDecision,
11
+ parseDeclaredValue,
12
+ validateDynamicOptions,
13
+ } from "./internal/evaluate.js";
14
+ import { getJson, postJson } from "./internal/http.js";
15
+ import { validateRequirements } from "./internal/requirements.js";
16
+ import { resolveRuntimeContext, type BarekeyRuntimeContext } from "./internal/runtime.js";
17
+ import { MemoryCache } from "./internal/cache.js";
18
+ import { DEFAULT_TYPEGEN_TTL_MS, resolveTtlMilliseconds } from "./internal/ttl.js";
19
+ import {
20
+ resolveInstalledSdkGeneratedTypesPath,
21
+ type TypegenManifest,
22
+ writeInstalledSdkGeneratedTypes,
23
+ } from "./internal/typegen.js";
24
+ import type {
25
+ BarekeyClientOptions,
26
+ BarekeyEvaluatedValue,
27
+ BarekeyGeneratedTypeMap,
28
+ BarekeyGetOptions,
29
+ BarekeyJsonConfig,
30
+ BarekeyKnownKey,
31
+ BarekeyTypegenResult,
32
+ BarekeyVariableDefinition,
33
+ } from "./types.js";
34
+
35
+ type DefinitionsResponse = {
36
+ definitions: Array<BarekeyVariableDefinition>;
37
+ };
38
+
39
+ type EvaluateResponse = {
40
+ name: string;
41
+ kind: BarekeyEvaluatedValue["kind"];
42
+ declaredType: BarekeyEvaluatedValue["declaredType"];
43
+ value: string;
44
+ decision?: BarekeyEvaluatedValue["decision"];
45
+ };
46
+
47
+ type SharedTypegenWatcher = {
48
+ intervalMs: number;
49
+ inFlight: Promise<void> | null;
50
+ warned: boolean;
51
+ timer: ReturnType<typeof setInterval>;
52
+ };
53
+
54
+ const sharedTypegenWatchers = new Map<string, SharedTypegenWatcher>();
55
+
56
+ function createDefaultFetch(): typeof globalThis.fetch {
57
+ if (typeof globalThis.fetch === "function") {
58
+ return globalThis.fetch.bind(globalThis);
59
+ }
60
+
61
+ return (async () => {
62
+ throw new NetworkError({
63
+ message: "fetch is not available in this runtime.",
64
+ });
65
+ }) as typeof globalThis.fetch;
66
+ }
67
+
68
+ function createTypegenInterval(callback: () => void, intervalMs: number): ReturnType<typeof setInterval> {
69
+ const timer = setInterval(callback, intervalMs);
70
+ if ("unref" in timer && typeof timer.unref === "function") {
71
+ timer.unref();
72
+ }
73
+ return timer;
74
+ }
75
+
76
+ function warnAutomaticTypegenFailure(error: unknown): void {
77
+ const message =
78
+ error instanceof Error ? error.message : "Barekey automatic typegen refresh failed.";
79
+ console.warn(`[barekey] Automatic typegen refresh failed.\n${message}`);
80
+ }
81
+
82
+ export class BarekeyClient {
83
+ private readonly options: BarekeyClientOptions;
84
+ private readonly fetchFn: typeof globalThis.fetch;
85
+ private readonly definitionCache = new MemoryCache<BarekeyVariableDefinition>();
86
+ private readonly evaluationCache = new MemoryCache<BarekeyEvaluatedValue>();
87
+ private runtimeContextPromise: Promise<BarekeyRuntimeContext> | null = null;
88
+ private requirementsPromise: Promise<void> | null = null;
89
+ private typegenWatcherStarted = false;
90
+
91
+ constructor();
92
+ constructor(options: {
93
+ organization: string;
94
+ project: string;
95
+ environment: string;
96
+ requirements?: BarekeyClientOptions["requirements"];
97
+ typegen?: BarekeyClientOptions["typegen"];
98
+ });
99
+ constructor(options: {
100
+ json: BarekeyJsonConfig;
101
+ requirements?: BarekeyClientOptions["requirements"];
102
+ typegen?: BarekeyClientOptions["typegen"];
103
+ });
104
+ constructor(options: BarekeyClientOptions = {}) {
105
+ this.options = options;
106
+ this.fetchFn = createDefaultFetch();
107
+ }
108
+
109
+ get<TKey extends BarekeyKnownKey>(
110
+ name: TKey,
111
+ options?: BarekeyGetOptions,
112
+ ): BarekeyEnvHandle<BarekeyGeneratedTypeMap[TKey]>;
113
+ get(name: string, options?: BarekeyGetOptions): BarekeyEnvHandle<unknown>;
114
+ get(name: string, options?: BarekeyGetOptions): BarekeyEnvHandle<unknown> {
115
+ return new BarekeyEnvHandle(
116
+ async () => await this.resolveEvaluatedValue(name, options),
117
+ );
118
+ }
119
+
120
+ async typegen(): Promise<BarekeyTypegenResult> {
121
+ const context = await this.getRuntimeContext();
122
+ const generatedTypesPath = await resolveInstalledSdkGeneratedTypesPath();
123
+ if (generatedTypesPath === null) {
124
+ throw new FsNotAvailableError({
125
+ message: "Barekey could not update generated SDK types because filesystem access is unavailable.",
126
+ });
127
+ }
128
+
129
+ const manifest = await this.fetchTypegenManifest(context);
130
+ return await writeInstalledSdkGeneratedTypes(manifest);
131
+ }
132
+
133
+ private async getRuntimeContext(): Promise<BarekeyRuntimeContext> {
134
+ if (this.runtimeContextPromise === null) {
135
+ const runtimeContextPromise = resolveRuntimeContext(this.options, this.fetchFn);
136
+ runtimeContextPromise.catch(() => {
137
+ if (this.runtimeContextPromise === runtimeContextPromise) {
138
+ this.runtimeContextPromise = null;
139
+ }
140
+ });
141
+ this.runtimeContextPromise = runtimeContextPromise;
142
+ }
143
+ return await this.runtimeContextPromise;
144
+ }
145
+
146
+ private async fetchTypegenManifest(context?: BarekeyRuntimeContext): Promise<TypegenManifest> {
147
+ const resolvedContext = context ?? (await this.getRuntimeContext());
148
+ return await getJson<TypegenManifest>({
149
+ fetchFn: this.fetchFn,
150
+ baseUrl: resolvedContext.baseUrl,
151
+ path: `/v1/typegen/manifest?projectSlug=${encodeURIComponent(
152
+ resolvedContext.project,
153
+ )}&stageSlug=${encodeURIComponent(resolvedContext.environment)}&orgSlug=${encodeURIComponent(
154
+ resolvedContext.organization,
155
+ )}`,
156
+ auth: resolvedContext.auth,
157
+ });
158
+ }
159
+
160
+ private getTypegenIntervalMs(): number {
161
+ const typegenOptions = this.options.typegen;
162
+ if (typegenOptions === false) {
163
+ return 0;
164
+ }
165
+ if (typegenOptions?.ttl === undefined) {
166
+ return DEFAULT_TYPEGEN_TTL_MS;
167
+ }
168
+ return resolveTtlMilliseconds(typegenOptions.ttl, "typegen.ttl");
169
+ }
170
+
171
+ private async startTypegenWatcher(context: BarekeyRuntimeContext): Promise<void> {
172
+ if (this.options.typegen === false || context.environment !== "development") {
173
+ return;
174
+ }
175
+
176
+ const generatedTypesPath = await resolveInstalledSdkGeneratedTypesPath();
177
+ if (generatedTypesPath === null) {
178
+ throw new FsNotAvailableError({
179
+ message: "Automatic Barekey typegen refresh requires filesystem access.",
180
+ });
181
+ }
182
+
183
+ const intervalMs = this.getTypegenIntervalMs();
184
+ const watcherKey = [
185
+ context.baseUrl,
186
+ context.organization,
187
+ context.project,
188
+ context.environment,
189
+ generatedTypesPath,
190
+ ].join("|");
191
+
192
+ const runWatcher = async (watcher: SharedTypegenWatcher): Promise<void> => {
193
+ if (watcher.inFlight !== null) {
194
+ await watcher.inFlight;
195
+ return;
196
+ }
197
+
198
+ watcher.inFlight = (async () => {
199
+ try {
200
+ await this.typegen();
201
+ watcher.warned = false;
202
+ } catch (error: unknown) {
203
+ if (!watcher.warned) {
204
+ warnAutomaticTypegenFailure(error);
205
+ watcher.warned = true;
206
+ }
207
+ } finally {
208
+ watcher.inFlight = null;
209
+ }
210
+ })();
211
+
212
+ await watcher.inFlight;
213
+ };
214
+
215
+ const existingWatcher = sharedTypegenWatchers.get(watcherKey);
216
+ if (existingWatcher !== undefined) {
217
+ if (intervalMs < existingWatcher.intervalMs) {
218
+ clearInterval(existingWatcher.timer);
219
+ existingWatcher.intervalMs = intervalMs;
220
+ existingWatcher.timer = createTypegenInterval(() => {
221
+ void runWatcher(existingWatcher);
222
+ }, intervalMs);
223
+ }
224
+ return;
225
+ }
226
+
227
+ const watcher: SharedTypegenWatcher = {
228
+ intervalMs,
229
+ inFlight: null,
230
+ warned: false,
231
+ timer: createTypegenInterval(() => {
232
+ void runWatcher(watcher);
233
+ }, intervalMs),
234
+ };
235
+ sharedTypegenWatchers.set(watcherKey, watcher);
236
+ void runWatcher(watcher);
237
+ }
238
+
239
+ private ensureTypegenWatcher(context: BarekeyRuntimeContext): void {
240
+ if (this.typegenWatcherStarted || this.options.typegen === false || context.environment !== "development") {
241
+ return;
242
+ }
243
+
244
+ this.typegenWatcherStarted = true;
245
+ void this.startTypegenWatcher(context).catch((error: unknown) => {
246
+ warnAutomaticTypegenFailure(error);
247
+ });
248
+ }
249
+
250
+ private buildDefinitionCacheKey(context: BarekeyRuntimeContext, name: string): string {
251
+ return [context.organization, context.project, context.environment, name].join("|");
252
+ }
253
+
254
+ private buildEvaluationCacheKey(
255
+ context: BarekeyRuntimeContext,
256
+ name: string,
257
+ options?: BarekeyGetOptions,
258
+ ): string {
259
+ return [
260
+ context.organization,
261
+ context.project,
262
+ context.environment,
263
+ name,
264
+ options?.seed ?? "",
265
+ options?.key ?? "",
266
+ ].join("|");
267
+ }
268
+
269
+ private async fetchDefinitions(names?: Array<string>): Promise<Array<BarekeyVariableDefinition>> {
270
+ const context = await this.getRuntimeContext();
271
+ const response = await postJson<DefinitionsResponse>({
272
+ fetchFn: this.fetchFn,
273
+ baseUrl: context.baseUrl,
274
+ path: "/v1/env/definitions",
275
+ payload: {
276
+ orgSlug: context.organization,
277
+ projectSlug: context.project,
278
+ stageSlug: context.environment,
279
+ ...(names === undefined ? {} : { names }),
280
+ },
281
+ auth: context.auth,
282
+ });
283
+
284
+ for (const definition of response.definitions) {
285
+ this.definitionCache.set(this.buildDefinitionCacheKey(context, definition.name), definition);
286
+ }
287
+
288
+ return response.definitions;
289
+ }
290
+
291
+ private async ensureRequirementsValidated(): Promise<void> {
292
+ const context = await this.getRuntimeContext();
293
+ const requirements = context.requirements;
294
+ if (requirements === undefined) {
295
+ return;
296
+ }
297
+
298
+ if (this.requirementsPromise === null) {
299
+ const requirementsPromise = (async () => {
300
+ const definitions = await this.fetchDefinitions();
301
+ const values: Record<string, unknown> = {};
302
+ for (const definition of definitions) {
303
+ const evaluated = await evaluateDefinition(definition);
304
+ values[definition.name] = parseDeclaredValue(evaluated.value, evaluated.declaredType);
305
+ }
306
+ await validateRequirements(requirements, values);
307
+ })();
308
+ requirementsPromise.catch(() => {
309
+ if (this.requirementsPromise === requirementsPromise) {
310
+ this.requirementsPromise = null;
311
+ }
312
+ });
313
+ this.requirementsPromise = requirementsPromise;
314
+ }
315
+
316
+ await this.requirementsPromise;
317
+ }
318
+
319
+ private async getStaticDefinition(name: string): Promise<BarekeyVariableDefinition> {
320
+ await this.ensureRequirementsValidated();
321
+ const context = await this.getRuntimeContext();
322
+ const cacheKey = this.buildDefinitionCacheKey(context, name);
323
+ const cached = this.definitionCache.get(cacheKey);
324
+ if (cached !== null) {
325
+ return cached;
326
+ }
327
+
328
+ const definitions = await this.fetchDefinitions([name]);
329
+ const resolved = definitions[0];
330
+ if (resolved === undefined) {
331
+ throw new VariableNotFoundError();
332
+ }
333
+ return resolved;
334
+ }
335
+
336
+ private async resolveStaticValue(
337
+ name: string,
338
+ options?: BarekeyGetOptions,
339
+ ): Promise<BarekeyEvaluatedValue> {
340
+ const definition = await this.getStaticDefinition(name);
341
+ return await evaluateDefinition(definition, options);
342
+ }
343
+
344
+ private async resolveDynamicValue(
345
+ name: string,
346
+ options?: BarekeyGetOptions,
347
+ ): Promise<BarekeyEvaluatedValue> {
348
+ await this.ensureRequirementsValidated();
349
+ const context = await this.getRuntimeContext();
350
+ const cacheKey = this.buildEvaluationCacheKey(context, name, options);
351
+ const dynamic = options?.dynamic;
352
+ const dynamicTtlMs =
353
+ dynamic !== undefined && dynamic !== true
354
+ ? resolveTtlMilliseconds(dynamic.ttl, "dynamic.ttl")
355
+ : null;
356
+ if (dynamic !== true) {
357
+ const cached = this.evaluationCache.getRecord(cacheKey);
358
+ if (cached !== null && dynamicTtlMs !== null && Date.now() - cached.storedAtMs <= dynamicTtlMs) {
359
+ return cached.value;
360
+ }
361
+ }
362
+
363
+ let resolved: BarekeyEvaluatedValue;
364
+ try {
365
+ const evaluated = await postJson<EvaluateResponse>({
366
+ fetchFn: this.fetchFn,
367
+ baseUrl: context.baseUrl,
368
+ path: "/v1/env/evaluate",
369
+ payload: {
370
+ orgSlug: context.organization,
371
+ projectSlug: context.project,
372
+ stageSlug: context.environment,
373
+ name,
374
+ seed: options?.seed,
375
+ key: options?.key,
376
+ },
377
+ auth: context.auth,
378
+ });
379
+
380
+ resolved = {
381
+ name: evaluated.name,
382
+ kind: evaluated.kind,
383
+ declaredType: evaluated.declaredType,
384
+ value: evaluated.value,
385
+ decision: evaluated.decision,
386
+ selectedArm: inferSelectedArmFromDecision(evaluated.decision),
387
+ };
388
+ } catch (error: unknown) {
389
+ if (!(error instanceof BillingUnavailableError)) {
390
+ throw error;
391
+ }
392
+
393
+ const freshDefinitions = await this.fetchDefinitions([name]);
394
+ const freshDefinition = freshDefinitions[0];
395
+ if (freshDefinition === undefined) {
396
+ throw new VariableNotFoundError();
397
+ }
398
+ resolved = await evaluateDefinition(freshDefinition, options);
399
+ }
400
+
401
+ if (dynamicTtlMs !== null) {
402
+ // dynamic.ttl is evaluated per read, so keep the cached fetch time and let
403
+ // later calls decide whether the entry is still fresh for their requested ttl.
404
+ this.evaluationCache.set(cacheKey, resolved);
405
+ }
406
+
407
+ return resolved;
408
+ }
409
+
410
+ private async resolveEvaluatedValue(
411
+ name: string,
412
+ options?: BarekeyGetOptions,
413
+ ): Promise<BarekeyEvaluatedValue> {
414
+ const context = await this.getRuntimeContext();
415
+ this.ensureTypegenWatcher(context);
416
+ validateDynamicOptions(options);
417
+ if (options?.dynamic === undefined) {
418
+ return await this.resolveStaticValue(name, options);
419
+ }
420
+ return await this.resolveDynamicValue(name, options);
421
+ }
422
+ }