@infaris/flags 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/dist/adapter.cjs +388 -0
- package/dist/adapter.d.cts +38 -0
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.js +51 -0
- package/dist/chunk-LLGINDZS.js +105 -0
- package/dist/chunk-RZ6DL2QZ.js +221 -0
- package/dist/chunk-TRTQSARU.js +34 -0
- package/dist/client.cjs +214 -0
- package/dist/client.d.cts +55 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.js +110 -0
- package/dist/index.cjs +364 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +32 -0
- package/dist/react.cjs +60 -0
- package/dist/react.d.cts +17 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.js +34 -0
- package/dist/types-DMIusvFt.d.cts +86 -0
- package/dist/types-DMIusvFt.d.ts +86 -0
- package/package.json +103 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__commonJS,
|
|
3
|
+
__toESM
|
|
4
|
+
} from "./chunk-TRTQSARU.js";
|
|
5
|
+
|
|
6
|
+
// ../hosting-sdk/dist/index.cjs
|
|
7
|
+
var require_dist = __commonJS({
|
|
8
|
+
"../hosting-sdk/dist/index.cjs"(exports, module) {
|
|
9
|
+
"use strict";
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var index_exports = {};
|
|
28
|
+
__export(index_exports, {
|
|
29
|
+
InfarisError: () => InfarisError2
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var errorBrand = /* @__PURE__ */ Symbol.for("@infaris/hosting-sdk/error");
|
|
33
|
+
var InfarisError2 = class _InfarisError extends Error {
|
|
34
|
+
// SDK bundles contain separate copies; catch the shared base across bundles and module formats.
|
|
35
|
+
static [Symbol.hasInstance](value) {
|
|
36
|
+
if (this !== _InfarisError) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
37
|
+
return value instanceof Error && errorBrand in value && value[errorBrand] === true;
|
|
38
|
+
}
|
|
39
|
+
code;
|
|
40
|
+
status;
|
|
41
|
+
constructor(message, code = "sdk_error", status = null) {
|
|
42
|
+
super(message);
|
|
43
|
+
Object.defineProperty(this, errorBrand, { value: true });
|
|
44
|
+
this.name = "InfarisError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.status = status;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/types.ts
|
|
53
|
+
var import_hosting_sdk = __toESM(require_dist(), 1);
|
|
54
|
+
var FlagsError = class extends import_hosting_sdk.InfarisError {
|
|
55
|
+
constructor(message, status, code) {
|
|
56
|
+
super(message, code, status);
|
|
57
|
+
this.status = status;
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.name = "FlagsError";
|
|
60
|
+
}
|
|
61
|
+
status;
|
|
62
|
+
code;
|
|
63
|
+
};
|
|
64
|
+
function detectEnvironment(env = readEnv()) {
|
|
65
|
+
const raw = (env?.INFARIS_ENV ?? env?.VERCEL_ENV ?? env?.NODE_ENV ?? "production").toLowerCase();
|
|
66
|
+
if (raw === "preview") return "preview";
|
|
67
|
+
if (raw === "development" || raw === "dev" || raw === "test" || raw === "local") return "development";
|
|
68
|
+
return "production";
|
|
69
|
+
}
|
|
70
|
+
function readEnv() {
|
|
71
|
+
const p = globalThis.process;
|
|
72
|
+
return p?.env;
|
|
73
|
+
}
|
|
74
|
+
function parseSseVersions(buffer) {
|
|
75
|
+
const versions = [];
|
|
76
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
77
|
+
const rest = events.pop() ?? "";
|
|
78
|
+
for (const event of events) {
|
|
79
|
+
let name = "message";
|
|
80
|
+
let data = "";
|
|
81
|
+
for (const line of event.split(/\r?\n/)) {
|
|
82
|
+
if (line.startsWith("event:")) name = line.slice(6).trim();
|
|
83
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
84
|
+
}
|
|
85
|
+
if (name !== "version" || !data) continue;
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(data);
|
|
88
|
+
if (typeof parsed.version === "number") versions.push(parsed.version);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { versions, rest };
|
|
93
|
+
}
|
|
94
|
+
function trimSlash(url) {
|
|
95
|
+
return url.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
require_dist,
|
|
100
|
+
FlagsError,
|
|
101
|
+
detectEnvironment,
|
|
102
|
+
readEnv,
|
|
103
|
+
parseSseVersions,
|
|
104
|
+
trimSlash
|
|
105
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlagsError,
|
|
3
|
+
detectEnvironment,
|
|
4
|
+
parseSseVersions,
|
|
5
|
+
readEnv,
|
|
6
|
+
trimSlash
|
|
7
|
+
} from "./chunk-LLGINDZS.js";
|
|
8
|
+
|
|
9
|
+
// src/evaluate.ts
|
|
10
|
+
function fnv1a32(input) {
|
|
11
|
+
const bytes = new TextEncoder().encode(input);
|
|
12
|
+
let hash = 2166136261;
|
|
13
|
+
for (const byte of bytes) {
|
|
14
|
+
hash ^= byte;
|
|
15
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0;
|
|
18
|
+
}
|
|
19
|
+
function rolloutBucket(flagKey, userId) {
|
|
20
|
+
return fnv1a32(`${flagKey}:${userId}`) % 1e4;
|
|
21
|
+
}
|
|
22
|
+
function attributeOf(ctx, attribute) {
|
|
23
|
+
switch (attribute) {
|
|
24
|
+
case "email":
|
|
25
|
+
return ctx.email ? ctx.email.trim().toLowerCase() : void 0;
|
|
26
|
+
case "emailDomain": {
|
|
27
|
+
const at = ctx.email ? ctx.email.lastIndexOf("@") : -1;
|
|
28
|
+
return at >= 0 ? ctx.email.slice(at + 1).trim().toLowerCase() : void 0;
|
|
29
|
+
}
|
|
30
|
+
case "userId":
|
|
31
|
+
return ctx.userId || void 0;
|
|
32
|
+
case "org":
|
|
33
|
+
return ctx.org || void 0;
|
|
34
|
+
case "role":
|
|
35
|
+
return ctx.role || void 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var caseless = (attribute) => attribute === "email" || attribute === "emailDomain";
|
|
39
|
+
function ruleMatches(rule, ctx) {
|
|
40
|
+
const actual = attributeOf(ctx, rule.attribute);
|
|
41
|
+
if (actual === void 0) return false;
|
|
42
|
+
const values = caseless(rule.attribute) ? rule.values.map((v) => v.trim().toLowerCase()) : rule.values;
|
|
43
|
+
switch (rule.operator) {
|
|
44
|
+
case "in":
|
|
45
|
+
return values.includes(actual);
|
|
46
|
+
case "not_in":
|
|
47
|
+
return !values.includes(actual);
|
|
48
|
+
case "ends_with":
|
|
49
|
+
return values.some((v) => v.length > 0 && actual.endsWith(v));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function evaluateFlag(flag, environment, ctx) {
|
|
53
|
+
if (flag.killed) return { value: false, reason: "killed" };
|
|
54
|
+
const env = flag.environments[environment];
|
|
55
|
+
if (!env || !env.enabled) return { value: false, reason: "off" };
|
|
56
|
+
for (const rule of env.rules) {
|
|
57
|
+
if (ruleMatches(rule, ctx)) return { value: rule.serve, reason: "rule" };
|
|
58
|
+
}
|
|
59
|
+
if (env.rollout === null || env.rollout >= 100) return { value: true, reason: "fallthrough" };
|
|
60
|
+
if (env.rollout <= 0) return { value: false, reason: "rollout" };
|
|
61
|
+
if (!ctx.userId) return { value: false, reason: "no_user" };
|
|
62
|
+
return { value: rolloutBucket(flag.key, ctx.userId) < Math.round(env.rollout * 100), reason: "rollout" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/server.ts
|
|
66
|
+
var FlagsClient = class {
|
|
67
|
+
environment;
|
|
68
|
+
url;
|
|
69
|
+
key;
|
|
70
|
+
fetchImpl;
|
|
71
|
+
refreshMs;
|
|
72
|
+
onError;
|
|
73
|
+
snapshot = null;
|
|
74
|
+
etag = null;
|
|
75
|
+
checkedAt = 0;
|
|
76
|
+
loading = null;
|
|
77
|
+
streamAbort = null;
|
|
78
|
+
streaming = false;
|
|
79
|
+
closed = false;
|
|
80
|
+
listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
const env = readEnv();
|
|
83
|
+
const url = options.url ?? env?.INFARIS_FLAGS_URL;
|
|
84
|
+
const key = options.secretKey ?? env?.INFARIS_FLAGS_SECRET_KEY;
|
|
85
|
+
if (!url) throw new FlagsError("Missing url (set INFARIS_FLAGS_URL)", null, "missing_url");
|
|
86
|
+
if (!key) throw new FlagsError("Missing secret key (set INFARIS_FLAGS_SECRET_KEY)", null, "missing_key");
|
|
87
|
+
if (!key.startsWith("ifr_fsk_")) throw new FlagsError("The server client needs the secret key (ifr_fsk_\u2026)", null, "wrong_key");
|
|
88
|
+
this.url = trimSlash(url);
|
|
89
|
+
this.key = key;
|
|
90
|
+
this.environment = options.environment ?? detectEnvironment(env);
|
|
91
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
92
|
+
this.refreshMs = options.refreshIntervalMs ?? 3e4;
|
|
93
|
+
this.onError = options.onError ?? (() => void 0);
|
|
94
|
+
if (options.stream ?? true) this.streaming = true;
|
|
95
|
+
}
|
|
96
|
+
/** The version in memory (0 before the first load). */
|
|
97
|
+
get version() {
|
|
98
|
+
return this.snapshot?.version ?? 0;
|
|
99
|
+
}
|
|
100
|
+
/** Loads the first snapshot (and opens the stream). Every getter calls it, so calling it yourself is optional. */
|
|
101
|
+
async ready() {
|
|
102
|
+
if (this.snapshot) {
|
|
103
|
+
if (Date.now() - this.checkedAt > this.refreshMs && !this.loading) void this.refresh().catch(this.onError);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await this.refresh();
|
|
107
|
+
if (this.streaming && !this.streamAbort) this.openStream();
|
|
108
|
+
}
|
|
109
|
+
/** Fetches the snapshot when it changed (`If-None-Match`). */
|
|
110
|
+
async refresh() {
|
|
111
|
+
if (this.loading) return this.loading;
|
|
112
|
+
this.loading = (async () => {
|
|
113
|
+
try {
|
|
114
|
+
const res = await this.fetchImpl(`${this.url}/snapshot`, {
|
|
115
|
+
headers: { authorization: `Bearer ${this.key}`, ...this.etag ? { "if-none-match": this.etag } : {} }
|
|
116
|
+
});
|
|
117
|
+
this.checkedAt = Date.now();
|
|
118
|
+
if (res.status === 304 && this.snapshot) return;
|
|
119
|
+
if (!res.ok) throw new FlagsError(`Flags snapshot failed (HTTP ${res.status})`, res.status, "http_error");
|
|
120
|
+
const next = await res.json();
|
|
121
|
+
const changed = next.version !== this.snapshot?.version;
|
|
122
|
+
this.snapshot = next;
|
|
123
|
+
this.etag = res.headers.get("etag");
|
|
124
|
+
if (changed) for (const listener of this.listeners) listener(next.version);
|
|
125
|
+
} finally {
|
|
126
|
+
this.loading = null;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
return this.loading;
|
|
130
|
+
}
|
|
131
|
+
/** The value of a flag for this user; `defaultValue` for an unknown flag or before Hosting answered once. */
|
|
132
|
+
async getFlag(key, context = {}, defaultValue = false) {
|
|
133
|
+
try {
|
|
134
|
+
await this.ready();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.onError(error);
|
|
137
|
+
if (!this.snapshot) return defaultValue;
|
|
138
|
+
}
|
|
139
|
+
return this.getFlagSync(key, context, defaultValue);
|
|
140
|
+
}
|
|
141
|
+
/** From memory only (no request): for hot paths once `ready()` resolved. */
|
|
142
|
+
getFlagSync(key, context = {}, defaultValue = false) {
|
|
143
|
+
const result = this.evaluate(key, context);
|
|
144
|
+
return result ? result.value : defaultValue;
|
|
145
|
+
}
|
|
146
|
+
/** Value and reason, or null for an unknown flag. */
|
|
147
|
+
evaluate(key, context = {}) {
|
|
148
|
+
const flag = this.snapshot?.flags.find((f) => f.key === key);
|
|
149
|
+
return flag ? evaluateFlag(flag, this.environment, context) : null;
|
|
150
|
+
}
|
|
151
|
+
/** Every flag for this user. */
|
|
152
|
+
async getAll(context = {}) {
|
|
153
|
+
await this.ready().catch(this.onError);
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const flag of this.snapshot?.flags ?? []) out[flag.key] = evaluateFlag(flag, this.environment, context).value;
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** An Edge Config value. */
|
|
159
|
+
async getConfig(key, defaultValue) {
|
|
160
|
+
await this.ready().catch(this.onError);
|
|
161
|
+
const items = this.snapshot?.items ?? {};
|
|
162
|
+
return Object.prototype.hasOwnProperty.call(items, key) ? items[key] : defaultValue;
|
|
163
|
+
}
|
|
164
|
+
/** Called with the new version after every change that reached this process. */
|
|
165
|
+
onChange(listener) {
|
|
166
|
+
this.listeners.add(listener);
|
|
167
|
+
return () => this.listeners.delete(listener);
|
|
168
|
+
}
|
|
169
|
+
close() {
|
|
170
|
+
this.closed = true;
|
|
171
|
+
this.streamAbort?.abort();
|
|
172
|
+
this.streamAbort = null;
|
|
173
|
+
}
|
|
174
|
+
openStream() {
|
|
175
|
+
if (this.closed) return;
|
|
176
|
+
const abort = new AbortController();
|
|
177
|
+
this.streamAbort = abort;
|
|
178
|
+
void (async () => {
|
|
179
|
+
let delay = 1e3;
|
|
180
|
+
while (!abort.signal.aborted) {
|
|
181
|
+
try {
|
|
182
|
+
const res = await this.fetchImpl(`${this.url}/stream`, {
|
|
183
|
+
headers: { authorization: `Bearer ${this.key}`, accept: "text/event-stream" },
|
|
184
|
+
signal: abort.signal
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok || !res.body) throw new FlagsError(`Flags stream failed (HTTP ${res.status})`, res.status, "stream_failed");
|
|
187
|
+
delay = 1e3;
|
|
188
|
+
const reader = res.body.getReader();
|
|
189
|
+
const decoder = new TextDecoder();
|
|
190
|
+
let buffer = "";
|
|
191
|
+
for (; ; ) {
|
|
192
|
+
const { done, value } = await reader.read();
|
|
193
|
+
if (done) break;
|
|
194
|
+
buffer += decoder.decode(value, { stream: true });
|
|
195
|
+
const parsed = parseSseVersions(buffer);
|
|
196
|
+
buffer = parsed.rest;
|
|
197
|
+
if (parsed.versions.some((v) => v > this.version)) await this.refresh().catch(this.onError);
|
|
198
|
+
else this.checkedAt = Date.now();
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (abort.signal.aborted) return;
|
|
202
|
+
this.onError(error);
|
|
203
|
+
}
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
205
|
+
delay = Math.min(delay * 2, 3e4);
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function createFlagsClient(options = {}) {
|
|
211
|
+
return new FlagsClient(options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export {
|
|
215
|
+
fnv1a32,
|
|
216
|
+
rolloutBucket,
|
|
217
|
+
ruleMatches,
|
|
218
|
+
evaluateFlag,
|
|
219
|
+
FlagsClient,
|
|
220
|
+
createFlagsClient
|
|
221
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
try {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
} catch (e) {
|
|
11
|
+
throw mod = 0, e;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
+
mod
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
__commonJS,
|
|
33
|
+
__toESM
|
|
34
|
+
};
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
28
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
29
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
30
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
31
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
32
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
33
|
+
mod
|
|
34
|
+
));
|
|
35
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
|
+
|
|
37
|
+
// ../hosting-sdk/dist/index.cjs
|
|
38
|
+
var require_dist = __commonJS({
|
|
39
|
+
"../hosting-sdk/dist/index.cjs"(exports2, module2) {
|
|
40
|
+
"use strict";
|
|
41
|
+
var __defProp2 = Object.defineProperty;
|
|
42
|
+
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
|
43
|
+
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
44
|
+
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
45
|
+
var __export2 = (target, all) => {
|
|
46
|
+
for (var name in all)
|
|
47
|
+
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
48
|
+
};
|
|
49
|
+
var __copyProps2 = (to, from, except, desc) => {
|
|
50
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
51
|
+
for (let key of __getOwnPropNames2(from))
|
|
52
|
+
if (!__hasOwnProp2.call(to, key) && key !== except)
|
|
53
|
+
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
|
54
|
+
}
|
|
55
|
+
return to;
|
|
56
|
+
};
|
|
57
|
+
var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
58
|
+
var index_exports = {};
|
|
59
|
+
__export2(index_exports, {
|
|
60
|
+
InfarisError: () => InfarisError2
|
|
61
|
+
});
|
|
62
|
+
module2.exports = __toCommonJS2(index_exports);
|
|
63
|
+
var errorBrand = /* @__PURE__ */ Symbol.for("@infaris/hosting-sdk/error");
|
|
64
|
+
var InfarisError2 = class _InfarisError extends Error {
|
|
65
|
+
// SDK bundles contain separate copies; catch the shared base across bundles and module formats.
|
|
66
|
+
static [Symbol.hasInstance](value) {
|
|
67
|
+
if (this !== _InfarisError) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
68
|
+
return value instanceof Error && errorBrand in value && value[errorBrand] === true;
|
|
69
|
+
}
|
|
70
|
+
code;
|
|
71
|
+
status;
|
|
72
|
+
constructor(message, code = "sdk_error", status = null) {
|
|
73
|
+
super(message);
|
|
74
|
+
Object.defineProperty(this, errorBrand, { value: true });
|
|
75
|
+
this.name = "InfarisError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
this.status = status;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// src/client.ts
|
|
84
|
+
var client_exports = {};
|
|
85
|
+
__export(client_exports, {
|
|
86
|
+
BrowserFlags: () => BrowserFlags,
|
|
87
|
+
createBrowserFlags: () => createBrowserFlags
|
|
88
|
+
});
|
|
89
|
+
module.exports = __toCommonJS(client_exports);
|
|
90
|
+
|
|
91
|
+
// src/types.ts
|
|
92
|
+
var import_hosting_sdk = __toESM(require_dist(), 1);
|
|
93
|
+
var FlagsError = class extends import_hosting_sdk.InfarisError {
|
|
94
|
+
constructor(message, status, code) {
|
|
95
|
+
super(message, code, status);
|
|
96
|
+
this.status = status;
|
|
97
|
+
this.code = code;
|
|
98
|
+
this.name = "FlagsError";
|
|
99
|
+
}
|
|
100
|
+
status;
|
|
101
|
+
code;
|
|
102
|
+
};
|
|
103
|
+
function readEnv() {
|
|
104
|
+
const p = globalThis.process;
|
|
105
|
+
return p?.env;
|
|
106
|
+
}
|
|
107
|
+
function trimSlash(url) {
|
|
108
|
+
return url.replace(/\/+$/, "");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/client.ts
|
|
112
|
+
var BrowserFlags = class {
|
|
113
|
+
url;
|
|
114
|
+
key;
|
|
115
|
+
environment;
|
|
116
|
+
options;
|
|
117
|
+
fetchImpl;
|
|
118
|
+
state;
|
|
119
|
+
loaded = false;
|
|
120
|
+
loading = null;
|
|
121
|
+
source = null;
|
|
122
|
+
listeners = /* @__PURE__ */ new Set();
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
const env = readEnv();
|
|
125
|
+
const url = options.url ?? env?.NEXT_PUBLIC_INFARIS_FLAGS_URL;
|
|
126
|
+
const key = options.publishableKey ?? env?.NEXT_PUBLIC_INFARIS_FLAGS_KEY;
|
|
127
|
+
if (!url) throw new FlagsError("Missing url (set NEXT_PUBLIC_INFARIS_FLAGS_URL)", null, "missing_url");
|
|
128
|
+
if (!key) throw new FlagsError("Missing publishable key (set NEXT_PUBLIC_INFARIS_FLAGS_KEY)", null, "missing_key");
|
|
129
|
+
if (key.startsWith("ifr_fsk_")) throw new FlagsError("Never use the secret key in a browser; use the publishable key (ifr_fpk_\u2026)", null, "wrong_key");
|
|
130
|
+
this.url = trimSlash(url);
|
|
131
|
+
this.key = key;
|
|
132
|
+
this.environment = options.environment ?? "production";
|
|
133
|
+
this.options = options;
|
|
134
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
135
|
+
this.state = options.initial ?? { version: 0, flags: {}, items: {} };
|
|
136
|
+
this.loaded = !!options.initial;
|
|
137
|
+
}
|
|
138
|
+
get version() {
|
|
139
|
+
return this.state.version;
|
|
140
|
+
}
|
|
141
|
+
/** The current answer; stable between changes (safe for `useSyncExternalStore`). */
|
|
142
|
+
snapshot() {
|
|
143
|
+
return this.state;
|
|
144
|
+
}
|
|
145
|
+
async load() {
|
|
146
|
+
if (this.loading) return this.loading;
|
|
147
|
+
this.loading = (async () => {
|
|
148
|
+
try {
|
|
149
|
+
const token = await this.options.getAccessToken?.();
|
|
150
|
+
const res = await this.fetchImpl(`${this.url}/evaluate`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
authorization: `Bearer ${this.key}`,
|
|
154
|
+
"content-type": "application/json",
|
|
155
|
+
...token ? { "x-infaris-auth-token": token } : {}
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({ environment: this.environment, ...this.options.anonymousId ? { anonymousId: this.options.anonymousId } : {} })
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) throw new FlagsError(`Flags evaluate failed (HTTP ${res.status})`, res.status, "http_error");
|
|
160
|
+
this.state = await res.json();
|
|
161
|
+
this.loaded = true;
|
|
162
|
+
for (const listener of this.listeners) listener();
|
|
163
|
+
} finally {
|
|
164
|
+
this.loading = null;
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
return this.loading;
|
|
168
|
+
}
|
|
169
|
+
getFlag(key, defaultValue = false) {
|
|
170
|
+
return Object.prototype.hasOwnProperty.call(this.state.flags, key) ? this.state.flags[key] : defaultValue;
|
|
171
|
+
}
|
|
172
|
+
getConfig(key, defaultValue) {
|
|
173
|
+
return Object.prototype.hasOwnProperty.call(this.state.items, key) ? this.state.items[key] : defaultValue;
|
|
174
|
+
}
|
|
175
|
+
/** Loads once, listens for new versions while anyone is subscribed. */
|
|
176
|
+
subscribe(listener) {
|
|
177
|
+
this.listeners.add(listener);
|
|
178
|
+
if (!this.loaded) void this.load().catch(() => void 0);
|
|
179
|
+
this.openStream();
|
|
180
|
+
return () => {
|
|
181
|
+
this.listeners.delete(listener);
|
|
182
|
+
if (this.listeners.size === 0) {
|
|
183
|
+
this.source?.close();
|
|
184
|
+
this.source = null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Ask again, e.g. after sign-in or sign-out (the token changed). */
|
|
189
|
+
reload() {
|
|
190
|
+
return this.load();
|
|
191
|
+
}
|
|
192
|
+
openStream() {
|
|
193
|
+
if (this.source || this.options.stream === false) return;
|
|
194
|
+
const ES = globalThis.EventSource;
|
|
195
|
+
if (!ES) return;
|
|
196
|
+
const source = new ES(`${this.url}/stream?key=${encodeURIComponent(this.key)}`);
|
|
197
|
+
source.addEventListener("version", (event) => {
|
|
198
|
+
try {
|
|
199
|
+
const { version } = JSON.parse(event.data);
|
|
200
|
+
if (version > this.state.version) void this.load().catch(() => void 0);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
this.source = source;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function createBrowserFlags(options = {}) {
|
|
208
|
+
return new BrowserFlags(options);
|
|
209
|
+
}
|
|
210
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
211
|
+
0 && (module.exports = {
|
|
212
|
+
BrowserFlags,
|
|
213
|
+
createBrowserFlags
|
|
214
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { a as FlagEnvironment, E as EvaluatedFlags, b as FetchLike, C as ConfigValue } from './types-DMIusvFt.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The browser client of `@infaris/flags` (`@infaris/flags/client`). It uses the publishable key
|
|
5
|
+
* (`NEXT_PUBLIC_INFARIS_FLAGS_KEY`), so it only ever sees flags and values marked client-visible, already evaluated
|
|
6
|
+
* by Hosting: the targeting rules (e-mail addresses, organizations) never reach the browser.
|
|
7
|
+
*
|
|
8
|
+
* Targeting on Infaris Auth claims needs the signed-in user's access token (`getAccessToken`); Hosting verifies it.
|
|
9
|
+
* Without a token only the percentage rollout applies, bucketed on `anonymousId`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface BrowserFlagsOptions {
|
|
13
|
+
/** `NEXT_PUBLIC_INFARIS_FLAGS_URL` by default. */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** `NEXT_PUBLIC_INFARIS_FLAGS_KEY` by default (`ifr_fpk_…`). */
|
|
16
|
+
publishableKey?: string;
|
|
17
|
+
environment?: FlagEnvironment;
|
|
18
|
+
/** The Infaris Auth access token of the signed-in user, if any (for example `session?.access_token`). */
|
|
19
|
+
getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
20
|
+
/** A stable id for visitors without an account (e.g. from a first-party cookie); used for the rollout only. */
|
|
21
|
+
anonymousId?: string;
|
|
22
|
+
/** Values to show before the first answer (e.g. from server rendering). */
|
|
23
|
+
initial?: EvaluatedFlags;
|
|
24
|
+
/** Listen for changes with `EventSource` (default true where it exists). */
|
|
25
|
+
stream?: boolean;
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
}
|
|
28
|
+
type Listener = () => void;
|
|
29
|
+
declare class BrowserFlags {
|
|
30
|
+
private readonly url;
|
|
31
|
+
private readonly key;
|
|
32
|
+
private readonly environment;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private state;
|
|
36
|
+
private loaded;
|
|
37
|
+
private loading;
|
|
38
|
+
private source;
|
|
39
|
+
private listeners;
|
|
40
|
+
constructor(options?: BrowserFlagsOptions);
|
|
41
|
+
get version(): number;
|
|
42
|
+
/** The current answer; stable between changes (safe for `useSyncExternalStore`). */
|
|
43
|
+
snapshot(): EvaluatedFlags;
|
|
44
|
+
load(): Promise<void>;
|
|
45
|
+
getFlag(key: string, defaultValue?: boolean): boolean;
|
|
46
|
+
getConfig<T extends ConfigValue = ConfigValue>(key: string, defaultValue?: T): T | undefined;
|
|
47
|
+
/** Loads once, listens for new versions while anyone is subscribed. */
|
|
48
|
+
subscribe(listener: Listener): () => void;
|
|
49
|
+
/** Ask again, e.g. after sign-in or sign-out (the token changed). */
|
|
50
|
+
reload(): Promise<void>;
|
|
51
|
+
private openStream;
|
|
52
|
+
}
|
|
53
|
+
declare function createBrowserFlags(options?: BrowserFlagsOptions): BrowserFlags;
|
|
54
|
+
|
|
55
|
+
export { BrowserFlags, type BrowserFlagsOptions, ConfigValue, EvaluatedFlags, FlagEnvironment, createBrowserFlags };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { a as FlagEnvironment, E as EvaluatedFlags, b as FetchLike, C as ConfigValue } from './types-DMIusvFt.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The browser client of `@infaris/flags` (`@infaris/flags/client`). It uses the publishable key
|
|
5
|
+
* (`NEXT_PUBLIC_INFARIS_FLAGS_KEY`), so it only ever sees flags and values marked client-visible, already evaluated
|
|
6
|
+
* by Hosting: the targeting rules (e-mail addresses, organizations) never reach the browser.
|
|
7
|
+
*
|
|
8
|
+
* Targeting on Infaris Auth claims needs the signed-in user's access token (`getAccessToken`); Hosting verifies it.
|
|
9
|
+
* Without a token only the percentage rollout applies, bucketed on `anonymousId`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface BrowserFlagsOptions {
|
|
13
|
+
/** `NEXT_PUBLIC_INFARIS_FLAGS_URL` by default. */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** `NEXT_PUBLIC_INFARIS_FLAGS_KEY` by default (`ifr_fpk_…`). */
|
|
16
|
+
publishableKey?: string;
|
|
17
|
+
environment?: FlagEnvironment;
|
|
18
|
+
/** The Infaris Auth access token of the signed-in user, if any (for example `session?.access_token`). */
|
|
19
|
+
getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
20
|
+
/** A stable id for visitors without an account (e.g. from a first-party cookie); used for the rollout only. */
|
|
21
|
+
anonymousId?: string;
|
|
22
|
+
/** Values to show before the first answer (e.g. from server rendering). */
|
|
23
|
+
initial?: EvaluatedFlags;
|
|
24
|
+
/** Listen for changes with `EventSource` (default true where it exists). */
|
|
25
|
+
stream?: boolean;
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
}
|
|
28
|
+
type Listener = () => void;
|
|
29
|
+
declare class BrowserFlags {
|
|
30
|
+
private readonly url;
|
|
31
|
+
private readonly key;
|
|
32
|
+
private readonly environment;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private state;
|
|
36
|
+
private loaded;
|
|
37
|
+
private loading;
|
|
38
|
+
private source;
|
|
39
|
+
private listeners;
|
|
40
|
+
constructor(options?: BrowserFlagsOptions);
|
|
41
|
+
get version(): number;
|
|
42
|
+
/** The current answer; stable between changes (safe for `useSyncExternalStore`). */
|
|
43
|
+
snapshot(): EvaluatedFlags;
|
|
44
|
+
load(): Promise<void>;
|
|
45
|
+
getFlag(key: string, defaultValue?: boolean): boolean;
|
|
46
|
+
getConfig<T extends ConfigValue = ConfigValue>(key: string, defaultValue?: T): T | undefined;
|
|
47
|
+
/** Loads once, listens for new versions while anyone is subscribed. */
|
|
48
|
+
subscribe(listener: Listener): () => void;
|
|
49
|
+
/** Ask again, e.g. after sign-in or sign-out (the token changed). */
|
|
50
|
+
reload(): Promise<void>;
|
|
51
|
+
private openStream;
|
|
52
|
+
}
|
|
53
|
+
declare function createBrowserFlags(options?: BrowserFlagsOptions): BrowserFlags;
|
|
54
|
+
|
|
55
|
+
export { BrowserFlags, type BrowserFlagsOptions, ConfigValue, EvaluatedFlags, FlagEnvironment, createBrowserFlags };
|