@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
@@ -0,0 +1,232 @@
1
+ import {
2
+ CoerceFailedError,
3
+ parseBigIntOrThrow,
4
+ parseBooleanOrThrow,
5
+ parseDateOrThrow,
6
+ parseFloatOrThrow,
7
+ parseJsonOrThrow,
8
+ } from "../errors.js";
9
+ import { resolveTtlMilliseconds } from "./ttl.js";
10
+ import type {
11
+ BarekeyCoerceTarget,
12
+ BarekeyDeclaredType,
13
+ BarekeyDecision,
14
+ BarekeyEvaluatedValue,
15
+ BarekeyGetOptions,
16
+ BarekeyRolloutMilestone,
17
+ BarekeyVariableDefinition,
18
+ } from "../types.js";
19
+
20
+ function parseRolloutInstant(value: string): number {
21
+ const parsed = Date.parse(value);
22
+ if (!Number.isFinite(parsed)) {
23
+ throw new CoerceFailedError({
24
+ message: `Invalid Barekey rollout milestone instant "${value}".`,
25
+ });
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ function normalizeRolloutMilestones(
31
+ value: Array<BarekeyRolloutMilestone>,
32
+ ): Array<BarekeyRolloutMilestone> {
33
+ if (value.length === 0) {
34
+ throw new CoerceFailedError({
35
+ message: "Rollout milestones must contain at least one entry.",
36
+ });
37
+ }
38
+
39
+ let previousAtMs = -Infinity;
40
+ return value.map((milestone) => {
41
+ if (
42
+ !Number.isFinite(milestone.percentage) ||
43
+ milestone.percentage < 0 ||
44
+ milestone.percentage > 100
45
+ ) {
46
+ throw new CoerceFailedError({
47
+ message: "Rollout milestone percentages must be between 0 and 100.",
48
+ });
49
+ }
50
+
51
+ const atMs = parseRolloutInstant(milestone.at);
52
+ if (atMs <= previousAtMs) {
53
+ throw new CoerceFailedError({
54
+ message: "Rollout milestones must be strictly increasing by time.",
55
+ });
56
+ }
57
+ previousAtMs = atMs;
58
+
59
+ return {
60
+ at: new Date(atMs).toISOString(),
61
+ percentage: milestone.percentage,
62
+ };
63
+ });
64
+ }
65
+
66
+ export function validateDynamicOptions(options?: BarekeyGetOptions): void {
67
+ if (options?.dynamic === undefined || options.dynamic === true) {
68
+ return;
69
+ }
70
+ resolveTtlMilliseconds(options.dynamic.ttl, "dynamic.ttl");
71
+ }
72
+
73
+ async function deterministicBucket(input: string): Promise<number> {
74
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
75
+ const bytes = new Uint8Array(digest);
76
+ const value =
77
+ ((bytes[0] ?? 0) << 24) | ((bytes[1] ?? 0) << 16) | ((bytes[2] ?? 0) << 8) | (bytes[3] ?? 0);
78
+ return (value >>> 0) / 4294967296;
79
+ }
80
+
81
+ function resolveLinearRolloutChance(input: {
82
+ milestones: Array<BarekeyRolloutMilestone>;
83
+ nowMs: number;
84
+ }): number {
85
+ const milestones = normalizeRolloutMilestones(input.milestones);
86
+ const first = milestones[0];
87
+ if (first === undefined) {
88
+ return 0;
89
+ }
90
+
91
+ if (input.nowMs < parseRolloutInstant(first.at)) {
92
+ return 0;
93
+ }
94
+
95
+ for (let index = 0; index < milestones.length - 1; index += 1) {
96
+ const current = milestones[index];
97
+ const next = milestones[index + 1];
98
+ if (current === undefined || next === undefined) {
99
+ continue;
100
+ }
101
+
102
+ const currentAtMs = parseRolloutInstant(current.at);
103
+ const nextAtMs = parseRolloutInstant(next.at);
104
+ if (input.nowMs >= currentAtMs && input.nowMs < nextAtMs) {
105
+ const progress = (input.nowMs - currentAtMs) / (nextAtMs - currentAtMs);
106
+ const percentage = current.percentage + (next.percentage - current.percentage) * progress;
107
+ return percentage / 100;
108
+ }
109
+ }
110
+
111
+ const last = milestones[milestones.length - 1];
112
+ return last === undefined ? 0 : last.percentage / 100;
113
+ }
114
+
115
+ export async function evaluateDefinition(
116
+ definition: BarekeyVariableDefinition,
117
+ options?: Pick<BarekeyGetOptions, "seed" | "key">,
118
+ ): Promise<BarekeyEvaluatedValue> {
119
+ if (definition.kind === "secret") {
120
+ return {
121
+ name: definition.name,
122
+ kind: definition.kind,
123
+ declaredType: definition.declaredType,
124
+ value: definition.value,
125
+ };
126
+ }
127
+
128
+ const seed = options?.seed?.trim() ?? "";
129
+ const key = options?.key?.trim() ?? "";
130
+ const bucket =
131
+ seed.length > 0 || key.length > 0
132
+ ? await deterministicBucket(`${definition.kind}:${definition.name}:${seed}:${key}`)
133
+ : Math.random();
134
+
135
+ if (definition.kind === "ab_roll") {
136
+ const selectedArm = bucket < definition.chance ? "A" : "B";
137
+ return {
138
+ name: definition.name,
139
+ kind: definition.kind,
140
+ declaredType: definition.declaredType,
141
+ value: selectedArm === "A" ? definition.valueA : definition.valueB,
142
+ selectedArm,
143
+ decision: {
144
+ bucket,
145
+ chance: definition.chance,
146
+ seed: seed.length > 0 ? seed : undefined,
147
+ key: key.length > 0 ? key : undefined,
148
+ matchedRule: "ab_roll",
149
+ },
150
+ };
151
+ }
152
+
153
+ const chance = resolveLinearRolloutChance({
154
+ milestones: definition.rolloutMilestones,
155
+ nowMs: Date.now(),
156
+ });
157
+ const selectedArm = bucket < chance ? "B" : "A";
158
+ return {
159
+ name: definition.name,
160
+ kind: definition.kind,
161
+ declaredType: definition.declaredType,
162
+ value: selectedArm === "A" ? definition.valueA : definition.valueB,
163
+ selectedArm,
164
+ decision: {
165
+ bucket,
166
+ chance,
167
+ seed: seed.length > 0 ? seed : undefined,
168
+ key: key.length > 0 ? key : undefined,
169
+ matchedRule: "linear_rollout",
170
+ },
171
+ };
172
+ }
173
+
174
+ export function inferSelectedArmFromDecision(decision?: BarekeyDecision): "A" | "B" | undefined {
175
+ if (decision === undefined) {
176
+ return undefined;
177
+ }
178
+ if (decision.matchedRule === "ab_roll") {
179
+ return decision.bucket < decision.chance ? "A" : "B";
180
+ }
181
+ return decision.bucket < decision.chance ? "B" : "A";
182
+ }
183
+
184
+ export function parseDeclaredValue(value: string, declaredType: BarekeyDeclaredType): unknown {
185
+ if (declaredType === "string") {
186
+ return value;
187
+ }
188
+ if (declaredType === "boolean") {
189
+ return parseBooleanOrThrow(value);
190
+ }
191
+ if (declaredType === "int64") {
192
+ return parseBigIntOrThrow(value);
193
+ }
194
+ if (declaredType === "float") {
195
+ return parseFloatOrThrow(value);
196
+ }
197
+ if (declaredType === "date") {
198
+ return parseDateOrThrow(value);
199
+ }
200
+ return parseJsonOrThrow(value);
201
+ }
202
+
203
+ export function coerceEvaluatedValue(
204
+ resolved: BarekeyEvaluatedValue,
205
+ target: BarekeyCoerceTarget,
206
+ ): unknown {
207
+ if (target === "string") {
208
+ return resolved.value;
209
+ }
210
+
211
+ if (target === "boolean" && resolved.selectedArm !== undefined) {
212
+ return resolved.selectedArm === "B";
213
+ }
214
+
215
+ if (target === "boolean") {
216
+ return parseBooleanOrThrow(resolved.value);
217
+ }
218
+
219
+ if (target === "int64") {
220
+ return parseBigIntOrThrow(resolved.value);
221
+ }
222
+
223
+ if (target === "float") {
224
+ return parseFloatOrThrow(resolved.value);
225
+ }
226
+
227
+ if (target === "date") {
228
+ return parseDateOrThrow(resolved.value);
229
+ }
230
+
231
+ return parseJsonOrThrow(resolved.value);
232
+ }
@@ -0,0 +1,134 @@
1
+ import { NetworkError, UnauthorizedError, createBarekeyErrorFromCode } from "../errors.js";
2
+
3
+ type ApiErrorPayload = {
4
+ error?: {
5
+ code?: string;
6
+ message?: string;
7
+ requestId?: string;
8
+ };
9
+ } | null;
10
+
11
+ export type InternalAuthResolver = {
12
+ getAccessToken(): Promise<string>;
13
+ onUnauthorized?(): Promise<void>;
14
+ };
15
+
16
+ async function parseJsonResponse(response: Response): Promise<unknown> {
17
+ try {
18
+ return await response.json();
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function normalizeBaseUrl(baseUrl: string): string {
25
+ return baseUrl.replace(/\/$/, "");
26
+ }
27
+
28
+ async function requestJson<TResponse>(input: {
29
+ fetchFn: typeof globalThis.fetch;
30
+ baseUrl: string;
31
+ path: string;
32
+ method: "GET" | "POST";
33
+ payload?: unknown;
34
+ auth?: InternalAuthResolver;
35
+ }): Promise<TResponse> {
36
+ const makeRequest = async (accessToken?: string): Promise<Response> =>
37
+ input.fetchFn(`${normalizeBaseUrl(input.baseUrl)}${input.path}`, {
38
+ method: input.method,
39
+ headers: {
40
+ ...(input.method === "POST"
41
+ ? {
42
+ "content-type": "application/json",
43
+ }
44
+ : {}),
45
+ ...(accessToken
46
+ ? {
47
+ authorization: `Bearer ${accessToken}`,
48
+ }
49
+ : {}),
50
+ },
51
+ ...(input.method === "POST"
52
+ ? {
53
+ body: JSON.stringify(input.payload),
54
+ }
55
+ : {}),
56
+ });
57
+
58
+ const accessToken = input.auth ? await input.auth.getAccessToken() : undefined;
59
+ let response: Response;
60
+ try {
61
+ response = await makeRequest(accessToken);
62
+ } catch (error: unknown) {
63
+ throw new NetworkError({
64
+ message: error instanceof Error ? error.message : "A Barekey network request failed.",
65
+ cause: error,
66
+ });
67
+ }
68
+
69
+ if (response.status === 401 && input.auth?.onUnauthorized) {
70
+ await input.auth.onUnauthorized();
71
+ const retryAccessToken = await input.auth.getAccessToken();
72
+ try {
73
+ response = await makeRequest(retryAccessToken);
74
+ } catch (error: unknown) {
75
+ throw new NetworkError({
76
+ message: error instanceof Error ? error.message : "A Barekey network request failed.",
77
+ cause: error,
78
+ });
79
+ }
80
+ }
81
+
82
+ const parsed = (await parseJsonResponse(response)) as ApiErrorPayload | TResponse;
83
+ if (!response.ok) {
84
+ const parsedError = parsed as ApiErrorPayload;
85
+ const code =
86
+ parsedError?.error?.code ?? (response.status === 401 ? "UNAUTHORIZED" : "UNKNOWN_ERROR");
87
+ const message =
88
+ parsedError?.error?.message ??
89
+ (response.status === 401
90
+ ? "The provided Barekey credentials were rejected."
91
+ : `Barekey request failed with status ${response.status}.`);
92
+
93
+ if (response.status === 401 && code === "UNAUTHORIZED" && !parsedError?.error?.message) {
94
+ throw new UnauthorizedError({
95
+ requestId: parsedError?.error?.requestId ?? null,
96
+ status: response.status,
97
+ });
98
+ }
99
+
100
+ throw createBarekeyErrorFromCode({
101
+ code,
102
+ message,
103
+ requestId: parsedError?.error?.requestId ?? null,
104
+ status: response.status,
105
+ });
106
+ }
107
+
108
+ return parsed as TResponse;
109
+ }
110
+
111
+ export async function postJson<TResponse>(input: {
112
+ fetchFn: typeof globalThis.fetch;
113
+ baseUrl: string;
114
+ path: string;
115
+ payload: unknown;
116
+ auth?: InternalAuthResolver;
117
+ }): Promise<TResponse> {
118
+ return await requestJson({
119
+ ...input,
120
+ method: "POST",
121
+ });
122
+ }
123
+
124
+ export async function getJson<TResponse>(input: {
125
+ fetchFn: typeof globalThis.fetch;
126
+ baseUrl: string;
127
+ path: string;
128
+ auth?: InternalAuthResolver;
129
+ }): Promise<TResponse> {
130
+ return await requestJson({
131
+ ...input,
132
+ method: "GET",
133
+ });
134
+ }