@shelbyb/daywalker-sdk 0.1.4 → 0.3.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 (55) hide show
  1. package/README.md +31 -2
  2. package/dist/analytics/index.d.ts +108 -0
  3. package/dist/analytics/index.js +371 -0
  4. package/dist/cli.js +5 -0
  5. package/dist/client.d.ts +15 -1
  6. package/dist/client.js +59 -12
  7. package/dist/config.d.ts +6 -0
  8. package/dist/config.js +22 -0
  9. package/dist/crypto/signatures.d.ts +18 -5
  10. package/dist/crypto/signatures.js +100 -41
  11. package/dist/flags/client.d.ts +102 -0
  12. package/dist/flags/client.js +302 -0
  13. package/dist/flags/common/events.d.ts +59 -0
  14. package/dist/flags/common/events.js +204 -0
  15. package/dist/flags/common/index.d.ts +5 -0
  16. package/dist/flags/common/index.js +12 -0
  17. package/dist/flags/common/sse.d.ts +62 -0
  18. package/dist/flags/common/sse.js +183 -0
  19. package/dist/flags/engine/bucket.d.ts +21 -0
  20. package/dist/flags/engine/bucket.js +41 -0
  21. package/dist/flags/engine/context.d.ts +30 -0
  22. package/dist/flags/engine/context.js +81 -0
  23. package/dist/flags/engine/evaluate.d.ts +54 -0
  24. package/dist/flags/engine/evaluate.js +177 -0
  25. package/dist/flags/engine/index.d.ts +7 -0
  26. package/dist/flags/engine/index.js +7 -0
  27. package/dist/flags/engine/operators.d.ts +13 -0
  28. package/dist/flags/engine/operators.js +139 -0
  29. package/dist/flags/engine/semver.d.ts +13 -0
  30. package/dist/flags/engine/semver.js +35 -0
  31. package/dist/flags/engine/sha1.d.ts +6 -0
  32. package/dist/flags/engine/sha1.js +43 -0
  33. package/dist/flags/engine/types.d.ts +120 -0
  34. package/dist/flags/engine/types.js +17 -0
  35. package/dist/flags/node.d.ts +97 -0
  36. package/dist/flags/node.js +263 -0
  37. package/dist/flags/react.d.ts +27 -0
  38. package/dist/flags/react.js +62 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/next.d.ts +15 -2
  42. package/dist/next.js +41 -9
  43. package/dist/openfeature/server.d.ts +26 -0
  44. package/dist/openfeature/server.js +42 -0
  45. package/dist/openfeature/shared.d.ts +19 -0
  46. package/dist/openfeature/shared.js +40 -0
  47. package/dist/openfeature/web.d.ts +29 -0
  48. package/dist/openfeature/web.js +50 -0
  49. package/dist/types.d.ts +3 -0
  50. package/docs/analytics.md +32 -0
  51. package/docs/flags-node.md +49 -0
  52. package/docs/flags-react.md +35 -0
  53. package/docs/flags.md +53 -0
  54. package/docs/openfeature.md +30 -0
  55. package/package.json +62 -7
@@ -0,0 +1,139 @@
1
+ import { compareSemVer, parseSemVer } from "./semver.js";
2
+ const regexCache = new Map();
3
+ /** `matches` limits: longer inputs or patterns never match. */
4
+ export const MAX_MATCH_INPUT_LENGTH = 1024;
5
+ export const MAX_MATCH_PATTERN_LENGTH = 500;
6
+ let warnedUnsafePattern = false;
7
+ /**
8
+ * Conservative ReDoS heuristic: a group that contains a quantifier or alternation and is itself
9
+ * repeated (`(a+)+`, `(a*)*`, `(a|aa)+`, `(?:x+y)*`, `(a+){2,}`). Character classes and escapes are skipped.
10
+ */
11
+ export function hasNestedQuantifier(pattern) {
12
+ const stack = []; // per open group: contains a quantifier / alternation
13
+ for (let i = 0; i < pattern.length; i++) {
14
+ const ch = pattern[i];
15
+ if (ch === "\\") {
16
+ i++;
17
+ }
18
+ else if (ch === "[") {
19
+ for (i++; i < pattern.length && pattern[i] !== "]"; i++)
20
+ if (pattern[i] === "\\")
21
+ i++;
22
+ }
23
+ else if (ch === "(") {
24
+ stack.push(false);
25
+ if (pattern[i + 1] === "?")
26
+ i++; // group modifier, not a quantifier
27
+ }
28
+ else if (ch === ")") {
29
+ const risky = stack.pop() ?? false;
30
+ const next = pattern[i + 1];
31
+ const repeated = next === "*" || next === "+" || next === "{";
32
+ if (risky && repeated)
33
+ return true;
34
+ if ((risky || repeated) && stack.length)
35
+ stack[stack.length - 1] = true;
36
+ }
37
+ else if ((ch === "*" || ch === "+" || ch === "?" || ch === "{" || ch === "|") && stack.length) {
38
+ stack[stack.length - 1] = true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ function regex(pattern) {
44
+ if (!regexCache.has(pattern)) {
45
+ let re = null;
46
+ if (pattern.length > MAX_MATCH_PATTERN_LENGTH || hasNestedQuantifier(pattern)) {
47
+ if (!warnedUnsafePattern) {
48
+ warnedUnsafePattern = true;
49
+ console.warn("DevPulse: a `matches` pattern was too long or had nested quantifiers and is treated as no match");
50
+ }
51
+ }
52
+ else {
53
+ try {
54
+ re = new RegExp(pattern);
55
+ }
56
+ catch {
57
+ re = null;
58
+ }
59
+ }
60
+ if (regexCache.size > 1000)
61
+ regexCache.clear();
62
+ regexCache.set(pattern, re);
63
+ }
64
+ return regexCache.get(pattern);
65
+ }
66
+ /** RFC 3339 timestamp or Unix milliseconds → epoch ms. */
67
+ export function parseTime(v) {
68
+ if (typeof v === "number" && Number.isFinite(v))
69
+ return v;
70
+ if (typeof v !== "string")
71
+ return null;
72
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/i.test(v))
73
+ return null;
74
+ const t = Date.parse(v);
75
+ return Number.isNaN(t) ? null : t;
76
+ }
77
+ function deepEqual(a, b) {
78
+ if (a === b)
79
+ return true;
80
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object")
81
+ return false;
82
+ if (Array.isArray(a) !== Array.isArray(b))
83
+ return false;
84
+ const ka = Object.keys(a);
85
+ const kb = Object.keys(b);
86
+ return ka.length === kb.length && ka.every((k) => deepEqual(a[k], b[k]));
87
+ }
88
+ /** Compare one context value with one clause value. Type mismatches never match. */
89
+ export function applyOperator(op, actual, expected) {
90
+ switch (op) {
91
+ case "in":
92
+ return deepEqual(actual, expected);
93
+ case "endsWith":
94
+ return typeof actual === "string" && typeof expected === "string" && actual.endsWith(expected);
95
+ case "startsWith":
96
+ return typeof actual === "string" && typeof expected === "string" && actual.startsWith(expected);
97
+ case "contains":
98
+ return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
99
+ case "matches": {
100
+ if (typeof actual !== "string" || typeof expected !== "string")
101
+ return false;
102
+ if (actual.length > MAX_MATCH_INPUT_LENGTH)
103
+ return false;
104
+ return regex(expected)?.test(actual) ?? false;
105
+ }
106
+ case "lessThan":
107
+ case "lessThanOrEqual":
108
+ case "greaterThan":
109
+ case "greaterThanOrEqual": {
110
+ if (typeof actual !== "number" || typeof expected !== "number")
111
+ return false;
112
+ if (op === "lessThan")
113
+ return actual < expected;
114
+ if (op === "lessThanOrEqual")
115
+ return actual <= expected;
116
+ if (op === "greaterThan")
117
+ return actual > expected;
118
+ return actual >= expected;
119
+ }
120
+ case "before":
121
+ case "after": {
122
+ const a = parseTime(actual);
123
+ const e = parseTime(expected);
124
+ if (a === null || e === null)
125
+ return false;
126
+ return op === "before" ? a < e : a > e;
127
+ }
128
+ case "semVerEqual":
129
+ case "semVerLessThan":
130
+ case "semVerGreaterThan": {
131
+ const a = parseSemVer(actual);
132
+ const e = parseSemVer(expected);
133
+ if (!a || !e)
134
+ return false;
135
+ const c = compareSemVer(a, e);
136
+ return op === "semVerEqual" ? c === 0 : op === "semVerLessThan" ? c < 0 : c > 0;
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Semantic Versioning 2.0.0 comparison. Like LaunchDarkly, versions missing a
3
+ * minor or patch component ("2" or "2.1") are accepted and padded with zeros.
4
+ */
5
+ export interface SemVer {
6
+ major: number;
7
+ minor: number;
8
+ patch: number;
9
+ prerelease: string[];
10
+ }
11
+ export declare function parseSemVer(input: unknown): SemVer | null;
12
+ /** Precedence per SemVer §11. Build metadata is ignored. */
13
+ export declare function compareSemVer(a: SemVer, b: SemVer): number;
@@ -0,0 +1,35 @@
1
+ const RE = /^v?(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
2
+ export function parseSemVer(input) {
3
+ if (typeof input !== "string")
4
+ return null;
5
+ const m = RE.exec(input.trim());
6
+ if (!m)
7
+ return null;
8
+ return { major: Number(m[1]), minor: Number(m[2] ?? 0), patch: Number(m[3] ?? 0), prerelease: m[4] ? m[4].split(".") : [] };
9
+ }
10
+ /** Precedence per SemVer §11. Build metadata is ignored. */
11
+ export function compareSemVer(a, b) {
12
+ for (const k of ["major", "minor", "patch"])
13
+ if (a[k] !== b[k])
14
+ return a[k] < b[k] ? -1 : 1;
15
+ if (!a.prerelease.length || !b.prerelease.length)
16
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
17
+ for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) {
18
+ const x = a.prerelease[i];
19
+ const y = b.prerelease[i];
20
+ if (x === undefined)
21
+ return -1;
22
+ if (y === undefined)
23
+ return 1;
24
+ if (x === y)
25
+ continue;
26
+ const xn = /^\d+$/.test(x);
27
+ const yn = /^\d+$/.test(y);
28
+ if (xn && yn)
29
+ return Number(x) < Number(y) ? -1 : 1;
30
+ if (xn !== yn)
31
+ return xn ? -1 : 1;
32
+ return x < y ? -1 : 1;
33
+ }
34
+ return 0;
35
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Synchronous SHA-1 (FIPS 180-4) over UTF-8 strings. Used only for
3
+ * deterministic bucketing, never for security, and kept dependency-free so the
4
+ * engine runs unchanged in Node, browsers, workers and edge runtimes.
5
+ */
6
+ export declare function sha1Hex(message: string): string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Synchronous SHA-1 (FIPS 180-4) over UTF-8 strings. Used only for
3
+ * deterministic bucketing, never for security, and kept dependency-free so the
4
+ * engine runs unchanged in Node, browsers, workers and edge runtimes.
5
+ */
6
+ export function sha1Hex(message) {
7
+ const bytes = new TextEncoder().encode(message);
8
+ const len = bytes.length;
9
+ const words = new Uint32Array((((len + 8) >> 6) + 1) * 16);
10
+ for (let i = 0; i < len; i++)
11
+ words[i >> 2] |= bytes[i] << (24 - (i % 4) * 8);
12
+ words[len >> 2] |= 0x80 << (24 - (len % 4) * 8);
13
+ const bitLen = len * 8;
14
+ words[words.length - 2] = Math.floor(bitLen / 0x100000000);
15
+ words[words.length - 1] = bitLen >>> 0;
16
+ let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0;
17
+ const w = new Uint32Array(80);
18
+ for (let block = 0; block < words.length; block += 16) {
19
+ for (let t = 0; t < 16; t++)
20
+ w[t] = words[block + t];
21
+ for (let t = 16; t < 80; t++) {
22
+ const x = w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16];
23
+ w[t] = (x << 1) | (x >>> 31);
24
+ }
25
+ let a = h0, b = h1, c = h2, d = h3, e = h4;
26
+ for (let t = 0; t < 80; t++) {
27
+ const f = t < 20 ? (b & c) | (~b & d) : t < 40 ? b ^ c ^ d : t < 60 ? (b & c) | (b & d) | (c & d) : b ^ c ^ d;
28
+ const k = t < 20 ? 0x5a827999 : t < 40 ? 0x6ed9eba1 : t < 60 ? 0x8f1bbcdc : 0xca62c1d6;
29
+ const temp = (((a << 5) | (a >>> 27)) + f + e + k + w[t]) >>> 0;
30
+ e = d;
31
+ d = c;
32
+ c = (b << 30) | (b >>> 2);
33
+ b = a;
34
+ a = temp;
35
+ }
36
+ h0 = (h0 + a) >>> 0;
37
+ h1 = (h1 + b) >>> 0;
38
+ h2 = (h2 + c) >>> 0;
39
+ h3 = (h3 + d) >>> 0;
40
+ h4 = (h4 + e) >>> 0;
41
+ }
42
+ return [h0, h1, h2, h3, h4].map((h) => (h >>> 0).toString(16).padStart(8, "0")).join("");
43
+ }
@@ -0,0 +1,120 @@
1
+ /** JSON-compatible flag values. */
2
+ export type FlagValue = boolean | string | number | null | FlagValue[] | {
3
+ [key: string]: FlagValue;
4
+ };
5
+ export declare const OPERATORS: readonly ["in", "endsWith", "startsWith", "matches", "contains", "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual", "before", "after", "semVerEqual", "semVerLessThan", "semVerGreaterThan", "segmentMatch"];
6
+ export type Operator = (typeof OPERATORS)[number];
7
+ export interface Clause {
8
+ /** Context kind the attribute belongs to. Defaults to "user". */
9
+ contextKind?: string;
10
+ /** Attribute name, or an RFC 6901 JSON Pointer when it starts with "/". */
11
+ attribute: string;
12
+ op: Operator;
13
+ values: FlagValue[];
14
+ negate?: boolean;
15
+ }
16
+ export interface WeightedVariation {
17
+ variation: number;
18
+ /** 0..100000 (thousandths of a percent). */
19
+ weight: number;
20
+ /** Excluded from experiment analysis even inside an experiment rollout. */
21
+ untracked?: boolean;
22
+ }
23
+ export interface Rollout {
24
+ kind?: "rollout" | "experiment";
25
+ contextKind?: string;
26
+ bucketBy?: string;
27
+ /** Experiments use a fixed seed so re-running a flag config keeps buckets stable. */
28
+ seed?: number;
29
+ variations: WeightedVariation[];
30
+ }
31
+ /** A variation index or a rollout. */
32
+ export interface VariationOrRollout {
33
+ variation?: number;
34
+ rollout?: Rollout;
35
+ }
36
+ export interface Rule extends VariationOrRollout {
37
+ id: string;
38
+ description?: string;
39
+ clauses: Clause[];
40
+ trackEvents?: boolean;
41
+ }
42
+ export interface Target {
43
+ contextKind?: string;
44
+ variation: number;
45
+ values: string[];
46
+ }
47
+ export interface Prerequisite {
48
+ key: string;
49
+ variation: number;
50
+ }
51
+ /** A flag as evaluated in one environment. */
52
+ export interface FlagConfig {
53
+ key: string;
54
+ version: number;
55
+ on: boolean;
56
+ salt: string;
57
+ variations: FlagValue[];
58
+ offVariation: number | null;
59
+ fallthrough: VariationOrRollout;
60
+ targets: Target[];
61
+ rules: Rule[];
62
+ prerequisites: Prerequisite[];
63
+ clientSide?: boolean;
64
+ trackEvents?: boolean;
65
+ trackEventsFallthrough?: boolean;
66
+ deleted?: boolean;
67
+ }
68
+ export interface SegmentRule {
69
+ id: string;
70
+ clauses: Clause[];
71
+ /** Optional: include only this fraction of matching contexts (0..100000). */
72
+ weight?: number;
73
+ rolloutContextKind?: string;
74
+ bucketBy?: string;
75
+ }
76
+ export interface SegmentConfig {
77
+ key: string;
78
+ version: number;
79
+ salt: string;
80
+ included: Target[] | {
81
+ contextKind?: string;
82
+ values: string[];
83
+ }[];
84
+ excluded: {
85
+ contextKind?: string;
86
+ values: string[];
87
+ }[];
88
+ rules: SegmentRule[];
89
+ deleted?: boolean;
90
+ }
91
+ export type ErrorKind = "FLAG_NOT_FOUND" | "MALFORMED_FLAG" | "USER_NOT_SPECIFIED" | "WRONG_TYPE" | "CLIENT_NOT_READY" | "EXCEPTION";
92
+ export type EvaluationReason = {
93
+ kind: "OFF";
94
+ } | {
95
+ kind: "FALLTHROUGH";
96
+ inExperiment?: boolean;
97
+ } | {
98
+ kind: "TARGET_MATCH";
99
+ } | {
100
+ kind: "RULE_MATCH";
101
+ ruleIndex: number;
102
+ ruleId: string;
103
+ inExperiment?: boolean;
104
+ } | {
105
+ kind: "PREREQUISITE_FAILED";
106
+ prerequisiteKey: string;
107
+ } | {
108
+ kind: "ERROR";
109
+ errorKind: ErrorKind;
110
+ };
111
+ export interface EvaluationDetail<T = FlagValue> {
112
+ value: T;
113
+ variationIndex: number | null;
114
+ reason: EvaluationReason;
115
+ }
116
+ /** Read-only access to flags and segments for one environment. */
117
+ export interface DataStore {
118
+ getFlag(key: string): FlagConfig | undefined;
119
+ getSegment(key: string): SegmentConfig | undefined;
120
+ }
@@ -0,0 +1,17 @@
1
+ export const OPERATORS = [
2
+ "in",
3
+ "endsWith",
4
+ "startsWith",
5
+ "matches",
6
+ "contains",
7
+ "lessThan",
8
+ "lessThanOrEqual",
9
+ "greaterThan",
10
+ "greaterThanOrEqual",
11
+ "before",
12
+ "after",
13
+ "semVerEqual",
14
+ "semVerLessThan",
15
+ "semVerGreaterThan",
16
+ "segmentMatch",
17
+ ];
@@ -0,0 +1,97 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { type ContextInput, type EvaluationDetail, type FlagConfig, type FlagValue, type SegmentConfig } from "./engine/index.js";
3
+ import { type Logger } from "./common/index.js";
4
+ export type { ContextInput, EvaluationDetail, FlagValue, Logger };
5
+ export interface NodeClientOptions {
6
+ /** Base URL of your DevPulse deployment (https required except on localhost). */
7
+ baseUrl: string;
8
+ /** Real-time updates over SSE (default). Set false to poll instead. */
9
+ stream?: boolean;
10
+ /** Poll interval when not streaming (and as a fallback). Minimum 30s. */
11
+ pollIntervalMs?: number;
12
+ sendEvents?: boolean;
13
+ flushIntervalMs?: number;
14
+ privateAttributes?: string[];
15
+ allAttributesPrivate?: boolean;
16
+ /** Never connect; every variation returns its default (useful in tests). */
17
+ offline?: boolean;
18
+ /** Start from fixed data (e.g. tests or a file); disables network. */
19
+ initialData?: {
20
+ flags?: Record<string, FlagConfig>;
21
+ segments?: Record<string, SegmentConfig>;
22
+ };
23
+ fetch?: typeof fetch;
24
+ logger?: Logger;
25
+ }
26
+ export interface AllFlagsState {
27
+ valid: boolean;
28
+ values: Record<string, FlagValue>;
29
+ /** Pass to the browser SDK's `bootstrap` option for flicker-free SSR. */
30
+ toJSON(): Record<string, {
31
+ value: FlagValue;
32
+ variation: number | null;
33
+ version: number;
34
+ reason?: unknown;
35
+ }>;
36
+ }
37
+ /**
38
+ * Server-side client. One instance per process and SDK key.
39
+ *
40
+ * const client = init(process.env.DEVPULSE_SDK_KEY!, { baseUrl });
41
+ * await client.waitForInitialization({ timeoutMs: 5000 });
42
+ * const enabled = await client.variation("new-checkout", { key: user.id, email: user.email }, false);
43
+ */
44
+ export declare class DevPulseNodeClient extends EventEmitter {
45
+ private readonly sdkKey;
46
+ private readonly options;
47
+ private store;
48
+ private stream;
49
+ private poller;
50
+ private etag;
51
+ private events;
52
+ private initialized;
53
+ private ready;
54
+ private resolveReady;
55
+ private rejectReady;
56
+ private streamFailures;
57
+ private readonly logger;
58
+ private readonly base;
59
+ constructor(sdkKey: string, options: NodeClientOptions);
60
+ private headers;
61
+ private startStreaming;
62
+ private startPolling;
63
+ private stopPolling;
64
+ private onStreamEvent;
65
+ private applyPut;
66
+ private emitUpdate;
67
+ private markReady;
68
+ private fail;
69
+ waitForInitialization(opts?: {
70
+ timeoutMs?: number;
71
+ }): Promise<this>;
72
+ get isInitialized(): boolean;
73
+ /** Evaluate a flag locally. Never throws; returns `defaultValue` on any problem. */
74
+ variationDetailSync<T extends FlagValue>(key: string, context: ContextInput, defaultValue: T): EvaluationDetail<T>;
75
+ variationSync<T extends FlagValue>(key: string, context: ContextInput, defaultValue: T): T;
76
+ variation<T extends FlagValue>(key: string, context: ContextInput, defaultValue: T): Promise<T>;
77
+ variationDetail<T extends FlagValue>(key: string, context: ContextInput, defaultValue: T): Promise<EvaluationDetail<T>>;
78
+ boolVariation(key: string, context: ContextInput, defaultValue: boolean): Promise<boolean>;
79
+ stringVariation(key: string, context: ContextInput, defaultValue: string): Promise<string>;
80
+ numberVariation(key: string, context: ContextInput, defaultValue: number): Promise<number>;
81
+ jsonVariation<T extends FlagValue>(key: string, context: ContextInput, defaultValue: T): Promise<T>;
82
+ /**
83
+ * Evaluate every flag for a context, e.g. to bootstrap a browser client
84
+ * during server rendering. Doesn't record analytics.
85
+ */
86
+ allFlagsState(context: ContextInput, opts?: {
87
+ clientSideOnly?: boolean;
88
+ withReasons?: boolean;
89
+ }): Promise<AllFlagsState>;
90
+ /** Secure mode hash for a context, for the browser SDK's `hash` option. */
91
+ secureModeHash(context: ContextInput): string;
92
+ track(key: string, context: ContextInput, data?: unknown, metricValue?: number): void;
93
+ identify(context: ContextInput): void;
94
+ flush(): Promise<void>;
95
+ close(): Promise<void>;
96
+ }
97
+ export declare function init(sdkKey: string, options: NodeClientOptions): DevPulseNodeClient;