@lunora/flags 1.0.0-alpha.16 → 1.0.0-alpha.17

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.mjs CHANGED
@@ -1,2 +1 @@
1
- export { defineFlags, isFlagsDefinition } from './packem_shared/defineFlags-DAIO8cWQ.mjs';
2
- export { createFlags } from './packem_shared/createFlags-DGosFql2.mjs';
1
+ import{defineFlags as o,isFlagsDefinition as r}from"./packem_shared/defineFlags-BO1q5hLr.mjs";import{createFlags as f}from"./packem_shared/createFlags-KrGb1nlE.mjs";export{f as createFlags,o as defineFlags,r as isFlagsDefinition};
@@ -0,0 +1 @@
1
+ import{LunoraError as k}from"@lunora/errors";import{OpenFeature as f,ErrorCode as E}from"@openfeature/server-sdk";const S=(e,o)=>e<o?-1:e>o?1:0,p=e=>{if(e===void 0)return"null";if(typeof e=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e))return`[${e.map(c=>p(c)).join(",")}]`;const o=Object.getPrototypeOf(e);if(o!==null&&o!==Object.prototype){const c=e.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${c} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const i=e,a=Object.keys(i).toSorted(S),s=[];for(const c of a){const b=i[c];b!==void 0&&s.push(`${JSON.stringify(c)}:${p(b)}`)}return`{${s.join(",")}}`},O="lunora";let u;const N=({hooks:e,logger:o,provider:i})=>(u===void 0&&(u=(async()=>{await f.setProviderAndWait(O,i());const a=f.getClient(O);return o&&a.setLogger(o),e&&e.length>0&&a.addHooks(...e),a})(),u.catch(()=>{u=void 0})),u),K=async()=>{u=void 0,await f.clearProviders()},$=(e,o,i,a)=>{const s=p([e,o,i]);return Object.keys(a).length===0?`${s}:{}`:`${s}:${p(a)}`},A=(e,o,i,a,s)=>{switch(o){case"boolean":return e.getBooleanDetails(i,a,s);case"number":return e.getNumberDetails(i,a,s);case"object":return e.getObjectDetails(i,a,s);case"string":return e.getStringDetails(i,a,s);default:throw new k("INTERNAL",`createFlags: unknown flag type "${o}"`)}},L=e=>{const{hooks:o,logger:i,provider:a,targetingKey:s}=e;let c;try{c=typeof s=="function"?s():s}catch{c=void 0}const b=new Map,g=(n,t,r,l)=>{const h=c===void 0?{...l}:{targetingKey:c,...l},w=y=>({errorCode:E.GENERAL,errorMessage:y instanceof Error?y.message:String(y),flagKey:t,flagMetadata:{},reason:"ERROR",value:r}),v=()=>N({hooks:o,logger:i,provider:a}).then(y=>A(y,n,t,r,h)).catch(w);let d;try{d=$(n,t,r,h)}catch{return v()}const j=b.get(d);if(j)return j;const m=v();return b.set(d,m),m};return{boolean:(n,t,r)=>g("boolean",n,t,r).then(l=>l.value),details:{boolean:(n,t,r)=>g("boolean",n,t,r),number:(n,t,r)=>g("number",n,t,r),object:(n,t,r)=>g("object",n,t,r),string:(n,t,r)=>g("string",n,t,r)},number:(n,t,r)=>g("number",n,t,r).then(l=>l.value),object:(n,t,r)=>g("object",n,t,r).then(l=>l.value),string:(n,t,r)=>g("string",n,t,r).then(l=>l.value)}};export{L as createFlags,K as resetFlags};
@@ -0,0 +1 @@
1
+ const e=i=>{if(typeof i.provider!="function")throw new TypeError('defineFlags: `provider` must be a function `(env) => Provider` (e.g. flagshipProvider({ binding: "FLAGS" }))');if(i.identify!==void 0&&typeof i.identify!="function")throw new TypeError("defineFlags: `identify` must be a function `(auth) => string | undefined` when provided");if(i.hooks!==void 0&&!Array.isArray(i.hooks))throw new TypeError("defineFlags: `hooks` must be an array of OpenFeature hooks when provided");return{...i,isLunoraFlags:!0}},r=i=>typeof i=="object"&&i!==null&&i.isLunoraFlags===!0;export{e as defineFlags,r as isFlagsDefinition};
@@ -1,75 +1 @@
1
- import { ErrorCode } from '@openfeature/server-sdk';
2
-
3
- const upperSnake = (flagKey) => flagKey.replaceAll(/[^a-z0-9]+/gi, "_").toUpperCase();
4
- const TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
5
- const FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
6
- const staticDetails = (value) => Promise.resolve({ reason: "STATIC", value });
7
- const missing = (defaultValue) => Promise.resolve({ reason: "DEFAULT", value: defaultValue });
8
- const parseError = (defaultValue, message) => Promise.resolve({ errorCode: ErrorCode.PARSE_ERROR, errorMessage: message, reason: "ERROR", value: defaultValue });
9
- const envProvider = (options = {}) => {
10
- const prefix = options.prefix ?? "FLAG_";
11
- const nameOf = options.name ?? ((flagKey) => prefix + upperSnake(flagKey));
12
- return (env) => {
13
- const raw = (flagKey) => {
14
- const value = env[nameOf(flagKey)];
15
- if (value === void 0 || value === null) {
16
- return void 0;
17
- }
18
- if (typeof value === "string") {
19
- return value;
20
- }
21
- if (typeof value === "number" || typeof value === "boolean") {
22
- return String(value);
23
- }
24
- return JSON.stringify(value);
25
- };
26
- return {
27
- metadata: { name: "lunora-env" },
28
- resolveBooleanEvaluation: (flagKey, defaultValue) => {
29
- const value = raw(flagKey);
30
- if (value === void 0) {
31
- return missing(defaultValue);
32
- }
33
- const token = value.trim().toLowerCase();
34
- if (TRUE_TOKENS.has(token)) {
35
- return staticDetails(true);
36
- }
37
- if (FALSE_TOKENS.has(token)) {
38
- return staticDetails(false);
39
- }
40
- return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) value is not a recognized boolean`);
41
- },
42
- resolveNumberEvaluation: (flagKey, defaultValue) => {
43
- const value = raw(flagKey);
44
- if (value === void 0) {
45
- return missing(defaultValue);
46
- }
47
- const parsed = Number(value);
48
- if (value.trim() === "" || Number.isNaN(parsed)) {
49
- return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) value is not a number`);
50
- }
51
- return staticDetails(parsed);
52
- },
53
- resolveObjectEvaluation: (flagKey, defaultValue) => {
54
- const value = raw(flagKey);
55
- if (value === void 0) {
56
- return missing(defaultValue);
57
- }
58
- try {
59
- return staticDetails(JSON.parse(value));
60
- } catch (error) {
61
- return parseError(
62
- defaultValue,
63
- `env flag "${flagKey}" (${nameOf(flagKey)}) is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
64
- );
65
- }
66
- },
67
- resolveStringEvaluation: (flagKey, defaultValue) => {
68
- const value = raw(flagKey);
69
- return value === void 0 ? missing(defaultValue) : staticDetails(value);
70
- }
71
- };
72
- };
73
- };
74
-
75
- export { envProvider };
1
+ import{ErrorCode as c}from"@openfeature/server-sdk";const m=o=>o.replaceAll(/[^a-z0-9]+/gi,"_").toUpperCase(),g=new Set(["1","on","true","yes"]),d=new Set(["0","false","no","off"]),a=o=>Promise.resolve({reason:"STATIC",value:o}),l=o=>Promise.resolve({reason:"DEFAULT",value:o}),f=(o,u)=>Promise.resolve({errorCode:c.PARSE_ERROR,errorMessage:u,reason:"ERROR",value:o}),E=(o={})=>{const u=o.prefix??"FLAG_",s=o.name??(v=>u+m(v));return v=>{const i=r=>{const e=v[s(r)];if(e!=null)return typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"?String(e):JSON.stringify(e)};return{metadata:{name:"lunora-env"},resolveBooleanEvaluation:(r,e)=>{const n=i(r);if(n===void 0)return l(e);const t=n.trim().toLowerCase();return g.has(t)?a(!0):d.has(t)?a(!1):f(e,`env flag "${r}" (${s(r)}) value is not a recognized boolean`)},resolveNumberEvaluation:(r,e)=>{const n=i(r);if(n===void 0)return l(e);const t=Number(n);return n.trim()===""||Number.isNaN(t)?f(e,`env flag "${r}" (${s(r)}) value is not a number`):a(t)},resolveObjectEvaluation:(r,e)=>{const n=i(r);if(n===void 0)return l(e);try{return a(JSON.parse(n))}catch(t){return f(e,`env flag "${r}" (${s(r)}) is not valid JSON: ${t instanceof Error?t.message:String(t)}`)}},resolveStringEvaluation:(r,e)=>{const n=i(r);return n===void 0?l(e):a(n)}}}};export{E as envProvider};
@@ -1,32 +1 @@
1
- import { FlagshipServerProvider } from '@cloudflare/flagship/server';
2
- import { LunoraError } from '@lunora/errors';
3
-
4
- const isBindingOptions = (options) => "binding" in options && typeof options.binding === "string";
5
- const flagshipProvider = (options) => {
6
- if (isBindingOptions(options)) {
7
- const { binding: bindingName, ...rest } = options;
8
- return (env) => {
9
- const binding = env[bindingName];
10
- if (binding === void 0 || binding === null) {
11
- throw new LunoraError(
12
- "INTERNAL",
13
- `flagshipProvider: no binding "${bindingName}" found on env. Add a \`flagship\` binding to wrangler.jsonc, e.g. { "flagship": [{ "binding": "${bindingName}", "app_id": "your-app-id" }] }.`
14
- );
15
- }
16
- return new FlagshipServerProvider({ binding, ...rest });
17
- };
18
- }
19
- const { appId, endpoint } = options;
20
- if (appId === void 0 && endpoint === void 0) {
21
- throw new LunoraError(
22
- "INTERNAL",
23
- 'flagshipProvider: HTTP mode requires either `appId` (the SDK builds the evaluation URL) or `endpoint` (a full evaluation URL). Pass exactly one, or use binding mode: `flagshipProvider({ binding: "FLAGS" })`.'
24
- );
25
- }
26
- if (appId !== void 0 && endpoint !== void 0) {
27
- throw new LunoraError("INTERNAL", "flagshipProvider: `appId` and `endpoint` are mutually exclusive in HTTP mode — pass exactly one.");
28
- }
29
- return () => new FlagshipServerProvider(options);
30
- };
31
-
32
- export { flagshipProvider };
1
+ import{FlagshipServerProvider as a}from"@cloudflare/flagship/server";import{LunoraError as e}from"@lunora/errors";const s=n=>"binding"in n&&typeof n.binding=="string",f=n=>{if(s(n)){const{binding:i,...t}=n;return p=>{const d=p[i];if(d==null)throw new e("INTERNAL",`flagshipProvider: no binding "${i}" found on env. Add a \`flagship\` binding to wrangler.jsonc, e.g. { "flagship": [{ "binding": "${i}", "app_id": "your-app-id" }] }.`);return new a({binding:d,...t})}}const{appId:o,endpoint:r}=n;if(o===void 0&&r===void 0)throw new e("INTERNAL",'flagshipProvider: HTTP mode requires either `appId` (the SDK builds the evaluation URL) or `endpoint` (a full evaluation URL). Pass exactly one, or use binding mode: `flagshipProvider({ binding: "FLAGS" })`.');if(o!==void 0&&r!==void 0)throw new e("INTERNAL","flagshipProvider: `appId` and `endpoint` are mutually exclusive in HTTP mode — pass exactly one.");return()=>new a(n)};export{f as flagshipProvider};
@@ -1,16 +1 @@
1
- import { TypedInMemoryProvider } from '@openfeature/server-sdk';
2
-
3
- const memoryProvider = (flags) => {
4
- const configuration = {};
5
- for (const [key, value] of Object.entries(flags)) {
6
- configuration[key] = {
7
- defaultVariant: "default",
8
- disabled: false,
9
- variants: { default: value }
10
- };
11
- }
12
- const provider = new TypedInMemoryProvider(configuration);
13
- return (_environment) => provider;
14
- };
15
-
16
- export { memoryProvider };
1
+ import{TypedInMemoryProvider as n}from"@openfeature/server-sdk";const d=t=>{const e={};for(const[r,a]of Object.entries(t))e[r]={defaultVariant:"default",disabled:!1,variants:{default:a}};const o=new n(e);return r=>o};export{d as memoryProvider};
package/dist/web.mjs CHANGED
@@ -1 +1 @@
1
- export { FlagshipClientProvider } from '@cloudflare/flagship/web';
1
+ import{FlagshipClientProvider as i}from"@cloudflare/flagship/web";export{i as FlagshipClientProvider};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/flags",
3
- "version": "1.0.0-alpha.16",
3
+ "version": "1.0.0-alpha.17",
4
4
  "description": "OpenFeature-based feature flags for Lunora — ctx.flags, useFlag, and a first-class Cloudflare Flagship provider with any OpenFeature provider pluggable",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -60,7 +60,7 @@
60
60
  "access": "public"
61
61
  },
62
62
  "dependencies": {
63
- "@lunora/errors": "1.0.0-alpha.8",
63
+ "@lunora/errors": "1.0.0-alpha.9",
64
64
  "@openfeature/server-sdk": "^1.22.0"
65
65
  },
66
66
  "peerDependencies": {
@@ -1,144 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { OpenFeature, ErrorCode } from '@openfeature/server-sdk';
3
-
4
- const compareKeys = (a, b) => {
5
- if (a < b) {
6
- return -1;
7
- }
8
- return a > b ? 1 : 0;
9
- };
10
- const stableStringify = (value) => {
11
- if (value === void 0) {
12
- return "null";
13
- }
14
- if (typeof value === "bigint") {
15
- throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");
16
- }
17
- if (value === null || typeof value !== "object") {
18
- return JSON.stringify(value);
19
- }
20
- if (Array.isArray(value)) {
21
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
22
- }
23
- const proto = Object.getPrototypeOf(value);
24
- if (proto !== null && proto !== Object.prototype) {
25
- const name = value.constructor?.name ?? "value";
26
- throw new TypeError(
27
- `stableStringify: cannot use a ${name} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`
28
- );
29
- }
30
- const record = value;
31
- const keys = Object.keys(record).toSorted(compareKeys);
32
- const parts = [];
33
- for (const key of keys) {
34
- const raw = record[key];
35
- if (raw === void 0) {
36
- continue;
37
- }
38
- parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
39
- }
40
- return `{${parts.join(",")}}`;
41
- };
42
-
43
- const DOMAIN = "lunora";
44
- let clientBinding;
45
- const bindClient = ({ hooks, logger, provider }) => {
46
- if (clientBinding === void 0) {
47
- clientBinding = (async () => {
48
- await OpenFeature.setProviderAndWait(DOMAIN, provider());
49
- const client = OpenFeature.getClient(DOMAIN);
50
- if (logger) {
51
- client.setLogger(logger);
52
- }
53
- if (hooks && hooks.length > 0) {
54
- client.addHooks(...hooks);
55
- }
56
- return client;
57
- })();
58
- clientBinding.catch(() => {
59
- clientBinding = void 0;
60
- });
61
- }
62
- return clientBinding;
63
- };
64
- const resetFlags = async () => {
65
- clientBinding = void 0;
66
- await OpenFeature.clearProviders();
67
- };
68
- const memoKey = (type, flagKey, defaultValue, context) => {
69
- const prefix = stableStringify([type, flagKey, defaultValue]);
70
- if (Object.keys(context).length === 0) {
71
- return `${prefix}:{}`;
72
- }
73
- return `${prefix}:${stableStringify(context)}`;
74
- };
75
- const resolveDetails = (client, type, flagKey, defaultValue, context) => {
76
- switch (type) {
77
- case "boolean": {
78
- return client.getBooleanDetails(flagKey, defaultValue, context);
79
- }
80
- case "number": {
81
- return client.getNumberDetails(flagKey, defaultValue, context);
82
- }
83
- case "object": {
84
- return client.getObjectDetails(flagKey, defaultValue, context);
85
- }
86
- case "string": {
87
- return client.getStringDetails(flagKey, defaultValue, context);
88
- }
89
- default: {
90
- throw new LunoraError("INTERNAL", `createFlags: unknown flag type "${type}"`);
91
- }
92
- }
93
- };
94
- const createFlags = (options) => {
95
- const { hooks, logger, provider, targetingKey } = options;
96
- let resolvedTargetingKey;
97
- try {
98
- resolvedTargetingKey = typeof targetingKey === "function" ? targetingKey() : targetingKey;
99
- } catch {
100
- resolvedTargetingKey = void 0;
101
- }
102
- const memo = /* @__PURE__ */ new Map();
103
- const evaluate = (type, flagKey, defaultValue, context) => {
104
- const merged = resolvedTargetingKey === void 0 ? { ...context } : { targetingKey: resolvedTargetingKey, ...context };
105
- const failClosed = (error) => {
106
- return {
107
- errorCode: ErrorCode.GENERAL,
108
- errorMessage: error instanceof Error ? error.message : String(error),
109
- flagKey,
110
- flagMetadata: {},
111
- reason: "ERROR",
112
- value: defaultValue
113
- };
114
- };
115
- const run = () => bindClient({ hooks, logger, provider }).then((client) => resolveDetails(client, type, flagKey, defaultValue, merged)).catch(failClosed);
116
- let key;
117
- try {
118
- key = memoKey(type, flagKey, defaultValue, merged);
119
- } catch {
120
- return run();
121
- }
122
- const cached = memo.get(key);
123
- if (cached) {
124
- return cached;
125
- }
126
- const pending = run();
127
- memo.set(key, pending);
128
- return pending;
129
- };
130
- return {
131
- boolean: (flagKey, defaultValue, context) => evaluate("boolean", flagKey, defaultValue, context).then((details) => details.value),
132
- details: {
133
- boolean: (flagKey, defaultValue, context) => evaluate("boolean", flagKey, defaultValue, context),
134
- number: (flagKey, defaultValue, context) => evaluate("number", flagKey, defaultValue, context),
135
- object: (flagKey, defaultValue, context) => evaluate("object", flagKey, defaultValue, context),
136
- string: (flagKey, defaultValue, context) => evaluate("string", flagKey, defaultValue, context)
137
- },
138
- number: (flagKey, defaultValue, context) => evaluate("number", flagKey, defaultValue, context).then((details) => details.value),
139
- object: (flagKey, defaultValue, context) => evaluate("object", flagKey, defaultValue, context).then((details) => details.value),
140
- string: (flagKey, defaultValue, context) => evaluate("string", flagKey, defaultValue, context).then((details) => details.value)
141
- };
142
- };
143
-
144
- export { createFlags, resetFlags };
@@ -1,15 +0,0 @@
1
- const defineFlags = (config) => {
2
- if (typeof config.provider !== "function") {
3
- throw new TypeError('defineFlags: `provider` must be a function `(env) => Provider` (e.g. flagshipProvider({ binding: "FLAGS" }))');
4
- }
5
- if (config.identify !== void 0 && typeof config.identify !== "function") {
6
- throw new TypeError("defineFlags: `identify` must be a function `(auth) => string | undefined` when provided");
7
- }
8
- if (config.hooks !== void 0 && !Array.isArray(config.hooks)) {
9
- throw new TypeError("defineFlags: `hooks` must be an array of OpenFeature hooks when provided");
10
- }
11
- return { ...config, isLunoraFlags: true };
12
- };
13
- const isFlagsDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraFlags === true;
14
-
15
- export { defineFlags, isFlagsDefinition };