@spotify-confidence/openfeature-server-provider-local 0.12.1 → 0.13.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/CHANGELOG.md +7 -0
- package/dist/index.fetch.js +1 -1
- package/dist/index.inlined.js +1 -1
- package/dist/index.node.js +1 -1
- package/dist/pages-router/api.d.ts +20 -0
- package/dist/pages-router/api.js +62 -0
- package/dist/pages-router/client.d.ts +69 -0
- package/dist/pages-router/client.js +32 -0
- package/dist/pages-router/server.d.ts +112 -0
- package/dist/pages-router/server.js +121 -0
- package/package.json +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.12.1...openfeature-provider-js-v0.13.0) (2026-05-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **js:** Next.js Pages Router support ([#393](https://github.com/spotify/confidence-resolver/issues/393)) ([8efd0ae](https://github.com/spotify/confidence-resolver/commit/8efd0aee9581edaddd531bc15ba63e741e598eff))
|
|
9
|
+
|
|
3
10
|
## [0.12.1](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.12.0...openfeature-provider-js-v0.12.1) (2026-04-28)
|
|
4
11
|
|
|
5
12
|
|
package/dist/index.fetch.js
CHANGED
|
@@ -1418,7 +1418,7 @@ function isObject(value) {
|
|
|
1418
1418
|
function isSet$3(value) {
|
|
1419
1419
|
return value !== null && value !== void 0;
|
|
1420
1420
|
}
|
|
1421
|
-
const VERSION = "0.
|
|
1421
|
+
const VERSION = "0.13.0";
|
|
1422
1422
|
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
1423
1423
|
const debugBackend = loadDebug();
|
|
1424
1424
|
const logger$2 = new class LoggerImpl {
|
package/dist/index.inlined.js
CHANGED
|
@@ -1418,7 +1418,7 @@ function isObject(value) {
|
|
|
1418
1418
|
function isSet$3(value) {
|
|
1419
1419
|
return value !== null && value !== void 0;
|
|
1420
1420
|
}
|
|
1421
|
-
const VERSION = "0.
|
|
1421
|
+
const VERSION = "0.13.0";
|
|
1422
1422
|
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
1423
1423
|
const debugBackend = loadDebug();
|
|
1424
1424
|
const logger$2 = new class LoggerImpl {
|
package/dist/index.node.js
CHANGED
|
@@ -1421,7 +1421,7 @@ function isObject(value) {
|
|
|
1421
1421
|
function isSet$3(value) {
|
|
1422
1422
|
return value !== null && value !== void 0;
|
|
1423
1423
|
}
|
|
1424
|
-
const VERSION = "0.
|
|
1424
|
+
const VERSION = "0.13.0";
|
|
1425
1425
|
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
1426
1426
|
const debugBackend = loadDebug();
|
|
1427
1427
|
const logger$2 = new class LoggerImpl {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextApiHandler } from "next";
|
|
2
|
+
|
|
3
|
+
//#region src/pages-router/api.d.ts
|
|
4
|
+
interface ApplyHandlerOptions {
|
|
5
|
+
/** Use a non-default OpenFeature provider by name. */
|
|
6
|
+
providerName?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds the POST handler that the client `ConfidencePagesProvider` calls to
|
|
10
|
+
* fire exposure events. Mount it at `/api/confidence/apply` (or pass a custom
|
|
11
|
+
* `apiPath` to `ConfidencePagesProvider` if you mount it elsewhere).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // pages/api/confidence/apply.ts
|
|
15
|
+
* import { applyHandler } from '@spotify-confidence/openfeature-server-provider-local/pages-router/api';
|
|
16
|
+
* export default applyHandler();
|
|
17
|
+
*/
|
|
18
|
+
declare function applyHandler(opts?: ApplyHandlerOptions): NextApiHandler;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ApplyHandlerOptions, applyHandler };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { OpenFeature } from "@openfeature/server-sdk";
|
|
2
|
+
import { createDecipheriv, createHash } from "node:crypto";
|
|
3
|
+
const PROVIDER_NAME = "ConfidenceServerProviderLocal";
|
|
4
|
+
function getConfidenceProvider(providerName) {
|
|
5
|
+
const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
|
|
6
|
+
if (provider?.metadata?.name !== PROVIDER_NAME) return null;
|
|
7
|
+
return provider;
|
|
8
|
+
}
|
|
9
|
+
const ALGO = "aes-256-gcm";
|
|
10
|
+
const IV_LEN = 12;
|
|
11
|
+
const TAG_LEN = 16;
|
|
12
|
+
let cachedKey = null;
|
|
13
|
+
function getKey() {
|
|
14
|
+
if (cachedKey) return cachedKey;
|
|
15
|
+
const raw = process.env.CONFIDENCE_TOKEN_KEY;
|
|
16
|
+
if (!raw) throw new Error("CONFIDENCE_TOKEN_KEY is not set. Generate one with: openssl rand -hex 32 — and set it in the server environment.");
|
|
17
|
+
cachedKey = createHash("sha256").update(raw).digest();
|
|
18
|
+
return cachedKey;
|
|
19
|
+
}
|
|
20
|
+
function openResolveToken(handle) {
|
|
21
|
+
const buf = Buffer.from(handle, "base64url");
|
|
22
|
+
if (buf.length < IV_LEN + TAG_LEN) throw new Error("Invalid Confidence handle");
|
|
23
|
+
const iv = buf.subarray(0, IV_LEN);
|
|
24
|
+
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
25
|
+
const ciphertext = buf.subarray(IV_LEN + TAG_LEN);
|
|
26
|
+
const decipher = createDecipheriv(ALGO, getKey(), iv);
|
|
27
|
+
decipher.setAuthTag(tag);
|
|
28
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
function applyHandler(opts = {}) {
|
|
31
|
+
return async (req, res) => {
|
|
32
|
+
if (req.method !== "POST") {
|
|
33
|
+
res.setHeader("Allow", "POST");
|
|
34
|
+
res.status(405).end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const body = req.body;
|
|
38
|
+
if (!body || typeof body.resolveToken !== "string" || typeof body.flagName !== "string") {
|
|
39
|
+
res.status(400).end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const provider = getConfidenceProvider(opts.providerName);
|
|
43
|
+
if (!provider) {
|
|
44
|
+
res.status(503).end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let openedToken;
|
|
48
|
+
try {
|
|
49
|
+
openedToken = openResolveToken(body.resolveToken);
|
|
50
|
+
} catch {
|
|
51
|
+
res.status(400).end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!openedToken) {
|
|
55
|
+
res.status(204).end();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
provider.applyFlag(openedToken, body.flagName);
|
|
59
|
+
res.status(204).end();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export { applyHandler };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import "@openfeature/core";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type ResolutionReason = "ERROR" | "FLAG_ARCHIVED" | "MATCH" | "NO_SEGMENT_MATCH" | "TARGETING_KEY_ERROR" | "NO_TREATMENT_MATCH" | "MATERIALIZATION_NOT_SUPPORTED" | "UNSPECIFIED";
|
|
6
|
+
declare enum ErrorCode {
|
|
7
|
+
PROVIDER_NOT_READY = "PROVIDER_NOT_READY",
|
|
8
|
+
PROVIDER_FATAL = "PROVIDER_FATAL",
|
|
9
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND",
|
|
10
|
+
TYPE_MISMATCH = "TYPE_MISMATCH",
|
|
11
|
+
GENERAL = "GENERAL",
|
|
12
|
+
}
|
|
13
|
+
interface ResolutionDetails<T> {
|
|
14
|
+
reason: ResolutionReason;
|
|
15
|
+
value: T;
|
|
16
|
+
variant?: string;
|
|
17
|
+
errorCode?: ErrorCode;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
shouldApply: boolean;
|
|
20
|
+
}
|
|
21
|
+
type FlagPrimitive = null | boolean | string | number;
|
|
22
|
+
type FlagObject = {
|
|
23
|
+
[key: string]: FlagValue;
|
|
24
|
+
};
|
|
25
|
+
type FlagValue = FlagPrimitive | FlagObject;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/flag-bundle.d.ts
|
|
28
|
+
interface FlagBundle {
|
|
29
|
+
flags: Record<string, ResolutionDetails<FlagObject | null> | undefined>;
|
|
30
|
+
resolveId: string;
|
|
31
|
+
resolveToken: string;
|
|
32
|
+
errorCode?: ErrorCode;
|
|
33
|
+
errorMessage?: string;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/pages-router/types.d.ts
|
|
37
|
+
type FlagBundle$1 = FlagBundle;
|
|
38
|
+
/**
|
|
39
|
+
* Server-to-client transport payload returned from `withConfidence` and
|
|
40
|
+
* consumed by `<ConfidencePagesProvider>`. Structurally a `FlagBundle` whose
|
|
41
|
+
* `resolveToken` has been sealed (AES-GCM) — the apply API route is the only
|
|
42
|
+
* thing that opens it.
|
|
43
|
+
*/
|
|
44
|
+
type ConfidencePageProps = FlagBundle$1;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/pages-router/client.d.ts
|
|
47
|
+
interface Props {
|
|
48
|
+
/**
|
|
49
|
+
* The Confidence payload returned from `withConfidence` in
|
|
50
|
+
* `getServerSideProps`, normally pulled out of `pageProps` in `_app.tsx`.
|
|
51
|
+
* Pages that don't resolve flags pass `undefined` and any `useFlag` /
|
|
52
|
+
* `useFlagDetails` calls in their tree return defaults.
|
|
53
|
+
*/
|
|
54
|
+
confidence?: ConfidencePageProps;
|
|
55
|
+
/** Override the apply API route. Must match where you mount `applyHandler`. */
|
|
56
|
+
apiPath?: string;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Place at the top of `_app.tsx`. Bridges the bundle resolved on the server
|
|
61
|
+
* to the client `useFlag` / `useFlagDetails` hooks.
|
|
62
|
+
*/
|
|
63
|
+
declare function ConfidencePagesProvider({
|
|
64
|
+
confidence,
|
|
65
|
+
apiPath,
|
|
66
|
+
children
|
|
67
|
+
}: Props): React.ReactElement;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { type ConfidencePageProps, ConfidencePagesProvider };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { ConfidenceClientProvider } from "@spotify-confidence/openfeature-server-provider-local/react-client";
|
|
4
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
5
|
+
const DEFAULT_APPLY_PATH = "/api/confidence/apply";
|
|
6
|
+
function ConfidencePagesProvider({ confidence, apiPath = DEFAULT_APPLY_PATH, children }) {
|
|
7
|
+
const resolveToken = confidence?.resolveToken;
|
|
8
|
+
const apply = useCallback(async (flagName) => {
|
|
9
|
+
if (!resolveToken) return;
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(apiPath, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
resolveToken,
|
|
16
|
+
flagName
|
|
17
|
+
}),
|
|
18
|
+
keepalive: true
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok && process.env.NODE_ENV !== "production") console.warn(`[Confidence] apply failed: ${res.status} ${res.statusText}. Mount applyHandler at ${apiPath} and ensure a ConfidenceServerProviderLocal is registered.`);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (process.env.NODE_ENV !== "production") console.warn("[Confidence] apply request failed:", err);
|
|
23
|
+
}
|
|
24
|
+
}, [resolveToken, apiPath]);
|
|
25
|
+
if (!confidence) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
26
|
+
return /* @__PURE__ */ jsx(ConfidenceClientProvider, {
|
|
27
|
+
bundle: confidence,
|
|
28
|
+
apply,
|
|
29
|
+
children
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export { ConfidencePagesProvider };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { EvaluationContext } from "@openfeature/server-sdk";
|
|
2
|
+
import { GetServerSideProps, GetServerSidePropsContext, Redirect } from "next";
|
|
3
|
+
import "@openfeature/core";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
type ResolutionReason = "ERROR" | "FLAG_ARCHIVED" | "MATCH" | "NO_SEGMENT_MATCH" | "TARGETING_KEY_ERROR" | "NO_TREATMENT_MATCH" | "MATERIALIZATION_NOT_SUPPORTED" | "UNSPECIFIED";
|
|
7
|
+
declare enum ErrorCode {
|
|
8
|
+
PROVIDER_NOT_READY = "PROVIDER_NOT_READY",
|
|
9
|
+
PROVIDER_FATAL = "PROVIDER_FATAL",
|
|
10
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND",
|
|
11
|
+
TYPE_MISMATCH = "TYPE_MISMATCH",
|
|
12
|
+
GENERAL = "GENERAL",
|
|
13
|
+
}
|
|
14
|
+
interface ResolutionDetails<T> {
|
|
15
|
+
reason: ResolutionReason;
|
|
16
|
+
value: T;
|
|
17
|
+
variant?: string;
|
|
18
|
+
errorCode?: ErrorCode;
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
shouldApply: boolean;
|
|
21
|
+
}
|
|
22
|
+
type FlagPrimitive = null | boolean | string | number;
|
|
23
|
+
type FlagObject = {
|
|
24
|
+
[key: string]: FlagValue;
|
|
25
|
+
};
|
|
26
|
+
type FlagValue = FlagPrimitive | FlagObject;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/flag-bundle.d.ts
|
|
29
|
+
interface FlagBundle {
|
|
30
|
+
flags: Record<string, ResolutionDetails<FlagObject | null> | undefined>;
|
|
31
|
+
resolveId: string;
|
|
32
|
+
resolveToken: string;
|
|
33
|
+
errorCode?: ErrorCode;
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/pages-router/types.d.ts
|
|
38
|
+
type FlagBundle$1 = FlagBundle;
|
|
39
|
+
/**
|
|
40
|
+
* Server-to-client transport payload returned from `withConfidence` and
|
|
41
|
+
* consumed by `<ConfidencePagesProvider>`. Structurally a `FlagBundle` whose
|
|
42
|
+
* `resolveToken` has been sealed (AES-GCM) — the apply API route is the only
|
|
43
|
+
* thing that opens it.
|
|
44
|
+
*/
|
|
45
|
+
type ConfidencePageProps = FlagBundle$1;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/pages-router/server.d.ts
|
|
48
|
+
interface WithConfidenceOptions {
|
|
49
|
+
/** Use a non-default OpenFeature provider by name. */
|
|
50
|
+
providerName?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Return type of the function passed to `withConfidence`. Augments the standard
|
|
54
|
+
* `getServerSideProps` shape with a required `context` (the evaluation context
|
|
55
|
+
* for flag resolution) and an optional `flags` allow-list.
|
|
56
|
+
*/
|
|
57
|
+
type WithConfidenceResult<P extends {
|
|
58
|
+
[key: string]: any;
|
|
59
|
+
confidence?: never;
|
|
60
|
+
}> = {
|
|
61
|
+
props: P | Promise<P>;
|
|
62
|
+
/**
|
|
63
|
+
* Evaluation context for flag resolution. Omit to skip flag resolution
|
|
64
|
+
* entirely for this request — `pageProps.confidence` will be absent and
|
|
65
|
+
* any `useFlag` / `useFlagDetails` calls in the tree fall back to
|
|
66
|
+
* default values. Useful when flag access is conditional (e.g.
|
|
67
|
+
* unauthenticated requests).
|
|
68
|
+
*/
|
|
69
|
+
context?: EvaluationContext;
|
|
70
|
+
/** Restrict resolution to a subset of flags. Defaults to all. */
|
|
71
|
+
flags?: string[];
|
|
72
|
+
} | {
|
|
73
|
+
redirect: Redirect;
|
|
74
|
+
} | {
|
|
75
|
+
notFound: true;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Low-level: resolve flags into a `ConfidencePageProps` payload. Use this if
|
|
79
|
+
* you want flag resolution outside the `withConfidence` flow (e.g. inside a
|
|
80
|
+
* custom decorator). Most callers should just use `withConfidence`.
|
|
81
|
+
*/
|
|
82
|
+
declare function resolveConfidence(context: EvaluationContext, opts?: {
|
|
83
|
+
flags?: string[];
|
|
84
|
+
providerName?: string;
|
|
85
|
+
}): Promise<ConfidencePageProps>;
|
|
86
|
+
/**
|
|
87
|
+
* `getServerSideProps` decorator. Pass a single function that does your data
|
|
88
|
+
* fetching AND returns the evaluation context for flag resolution. The
|
|
89
|
+
* decorator resolves the flag bundle, seals it, and merges it into
|
|
90
|
+
* `pageProps.confidence`.
|
|
91
|
+
*
|
|
92
|
+
* Returning `{ redirect }` or `{ notFound }` short-circuits before flag
|
|
93
|
+
* resolution, just like a normal `getServerSideProps`.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* export const getServerSideProps = withConfidence(async ({ req }) => {
|
|
97
|
+
* const visitorId = req.cookies.uid ?? 'anon';
|
|
98
|
+
* const data = await fetchSomeData();
|
|
99
|
+
* return {
|
|
100
|
+
* props: { data },
|
|
101
|
+
* context: { visitor_id: visitorId },
|
|
102
|
+
* };
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
declare function withConfidence<P extends {
|
|
106
|
+
[key: string]: any;
|
|
107
|
+
confidence?: never;
|
|
108
|
+
} = {}>(gssp: (ctx: GetServerSidePropsContext) => Promise<WithConfidenceResult<P>>, opts?: WithConfidenceOptions): GetServerSideProps<P & {
|
|
109
|
+
confidence?: ConfidencePageProps;
|
|
110
|
+
}>;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type ConfidencePageProps, WithConfidenceOptions, WithConfidenceResult, resolveConfidence, withConfidence };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import "@bufbuild/protobuf/wire";
|
|
2
|
+
import { OpenFeature } from "@openfeature/server-sdk";
|
|
3
|
+
import { createCipheriv, createHash, randomBytes } from "node:crypto";
|
|
4
|
+
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
5
|
+
const debugBackend = loadDebug();
|
|
6
|
+
const logger = new class LoggerImpl {
|
|
7
|
+
childLoggers = /* @__PURE__ */ new Map();
|
|
8
|
+
debug = NOOP_LOG_FN;
|
|
9
|
+
info = NOOP_LOG_FN;
|
|
10
|
+
warn = NOOP_LOG_FN;
|
|
11
|
+
error = NOOP_LOG_FN;
|
|
12
|
+
constructor(name) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.configure();
|
|
15
|
+
}
|
|
16
|
+
async configure(backend = debugBackend) {
|
|
17
|
+
const debug = await backend;
|
|
18
|
+
if (!debug) return;
|
|
19
|
+
const debugFn = this.debug = (debug(this.name + ":debug"));
|
|
20
|
+
const infoFn = this.info = (debug(this.name + ":info"));
|
|
21
|
+
const warnFn = this.warn = (debug(this.name + ":warn"));
|
|
22
|
+
const errorFn = this.error = (debug(this.name + ":error"));
|
|
23
|
+
switch (true) {
|
|
24
|
+
case debugFn.enabled: infoFn.enabled = true;
|
|
25
|
+
case infoFn.enabled: warnFn.enabled = true;
|
|
26
|
+
case warnFn.enabled: errorFn.enabled = true;
|
|
27
|
+
}
|
|
28
|
+
for (const child of this.childLoggers.values()) child.configure(debug);
|
|
29
|
+
}
|
|
30
|
+
getLogger(name) {
|
|
31
|
+
let child = (this.childLoggers.get(name));
|
|
32
|
+
if (!child) {
|
|
33
|
+
child = new LoggerImpl(this.name + ":" + name);
|
|
34
|
+
this.childLoggers.set(name, child);
|
|
35
|
+
}
|
|
36
|
+
return child;
|
|
37
|
+
}
|
|
38
|
+
}("cnfd");
|
|
39
|
+
logger.getLogger.bind(logger);
|
|
40
|
+
async function loadDebug() {
|
|
41
|
+
try {
|
|
42
|
+
const { default: debug } = await import("debug");
|
|
43
|
+
if (typeof debug !== "function") return null;
|
|
44
|
+
return debug;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let ErrorCode = /* @__PURE__ */ function(ErrorCode$1) {
|
|
50
|
+
ErrorCode$1["PROVIDER_NOT_READY"] = "PROVIDER_NOT_READY";
|
|
51
|
+
ErrorCode$1["PROVIDER_FATAL"] = "PROVIDER_FATAL";
|
|
52
|
+
ErrorCode$1["FLAG_NOT_FOUND"] = "FLAG_NOT_FOUND";
|
|
53
|
+
ErrorCode$1["TYPE_MISMATCH"] = "TYPE_MISMATCH";
|
|
54
|
+
ErrorCode$1["GENERAL"] = "GENERAL";
|
|
55
|
+
return ErrorCode$1;
|
|
56
|
+
}({});
|
|
57
|
+
function error(errorCode, errorMessage) {
|
|
58
|
+
return {
|
|
59
|
+
flags: {},
|
|
60
|
+
resolveId: "",
|
|
61
|
+
resolveToken: "",
|
|
62
|
+
errorCode,
|
|
63
|
+
errorMessage
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const PROVIDER_NAME = "ConfidenceServerProviderLocal";
|
|
67
|
+
function getConfidenceProvider(providerName) {
|
|
68
|
+
const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
|
|
69
|
+
if (provider?.metadata?.name !== PROVIDER_NAME) return null;
|
|
70
|
+
return provider;
|
|
71
|
+
}
|
|
72
|
+
const ALGO = "aes-256-gcm";
|
|
73
|
+
const IV_LEN = 12;
|
|
74
|
+
let cachedKey = null;
|
|
75
|
+
function getKey() {
|
|
76
|
+
if (cachedKey) return cachedKey;
|
|
77
|
+
const raw = process.env.CONFIDENCE_TOKEN_KEY;
|
|
78
|
+
if (!raw) throw new Error("CONFIDENCE_TOKEN_KEY is not set. Generate one with: openssl rand -hex 32 — and set it in the server environment.");
|
|
79
|
+
cachedKey = createHash("sha256").update(raw).digest();
|
|
80
|
+
return cachedKey;
|
|
81
|
+
}
|
|
82
|
+
function sealResolveToken(token) {
|
|
83
|
+
const iv = randomBytes(IV_LEN);
|
|
84
|
+
const cipher = createCipheriv(ALGO, getKey(), iv);
|
|
85
|
+
const ciphertext = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
|
|
86
|
+
const tag = cipher.getAuthTag();
|
|
87
|
+
return Buffer.concat([
|
|
88
|
+
iv,
|
|
89
|
+
tag,
|
|
90
|
+
ciphertext
|
|
91
|
+
]).toString("base64url");
|
|
92
|
+
}
|
|
93
|
+
function providerMissingBundle(providerName) {
|
|
94
|
+
return error(ErrorCode.GENERAL, `OpenFeature provider${providerName ? ` "${providerName}"` : ""} is not a ConfidenceServerProviderLocal. Register one with OpenFeature.setProviderAndWait — typically in instrumentation.ts.`);
|
|
95
|
+
}
|
|
96
|
+
async function resolveConfidence(context, opts = {}) {
|
|
97
|
+
const provider = getConfidenceProvider(opts.providerName);
|
|
98
|
+
if (!provider) return providerMissingBundle(opts.providerName);
|
|
99
|
+
const bundle = await provider.resolve(context, opts.flags ?? []);
|
|
100
|
+
return {
|
|
101
|
+
...bundle,
|
|
102
|
+
resolveToken: sealResolveToken(bundle.resolveToken)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function withConfidence(gssp, opts = {}) {
|
|
106
|
+
return async (ctx) => {
|
|
107
|
+
const result = await gssp(ctx);
|
|
108
|
+
if (!("props" in result)) return result;
|
|
109
|
+
const innerProps = await Promise.resolve(result.props);
|
|
110
|
+
if (result.context === void 0) return { props: innerProps };
|
|
111
|
+
const confidence = await resolveConfidence(result.context, {
|
|
112
|
+
flags: result.flags,
|
|
113
|
+
providerName: opts.providerName
|
|
114
|
+
});
|
|
115
|
+
return { props: {
|
|
116
|
+
...innerProps,
|
|
117
|
+
confidence
|
|
118
|
+
} };
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export { resolveConfidence, withConfidence };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spotify-confidence/openfeature-server-provider-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Spotify Confidence Open Feature provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
"types": "./dist/client.d.ts",
|
|
33
33
|
"default": "./dist/client.js"
|
|
34
34
|
},
|
|
35
|
+
"./pages-router/server": {
|
|
36
|
+
"types": "./dist/pages-router/server.d.ts",
|
|
37
|
+
"default": "./dist/pages-router/server.js"
|
|
38
|
+
},
|
|
39
|
+
"./pages-router/client": {
|
|
40
|
+
"types": "./dist/pages-router/client.d.ts",
|
|
41
|
+
"default": "./dist/pages-router/client.js"
|
|
42
|
+
},
|
|
43
|
+
"./pages-router/api": {
|
|
44
|
+
"types": "./dist/pages-router/api.d.ts",
|
|
45
|
+
"default": "./dist/pages-router/api.js"
|
|
46
|
+
},
|
|
35
47
|
"./package.json": "./package.json"
|
|
36
48
|
},
|
|
37
49
|
"scripts": {
|
|
@@ -60,6 +72,7 @@
|
|
|
60
72
|
"debug": "^4.4.3",
|
|
61
73
|
"dotenv": "^17.2.2",
|
|
62
74
|
"happy-dom": "^20.3.4",
|
|
75
|
+
"next": "^16.0.0",
|
|
63
76
|
"prettier": "^2.8.8",
|
|
64
77
|
"react": "^19",
|
|
65
78
|
"react-dom": "^19.2.3",
|
|
@@ -72,6 +85,7 @@
|
|
|
72
85
|
"peerDependencies": {
|
|
73
86
|
"@openfeature/core": "^1.0.0",
|
|
74
87
|
"debug": "^4.4.3",
|
|
88
|
+
"next": ">=13.0.0",
|
|
75
89
|
"react": "^18.0.0 || ^19.0.0"
|
|
76
90
|
},
|
|
77
91
|
"peerDependenciesMeta": {
|
|
@@ -81,6 +95,9 @@
|
|
|
81
95
|
"debug": {
|
|
82
96
|
"optional": true
|
|
83
97
|
},
|
|
98
|
+
"next": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
84
101
|
"react": {
|
|
85
102
|
"optional": true
|
|
86
103
|
}
|