@feelflow/ffid-sdk 2.19.0 → 2.21.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.
@@ -802,7 +802,7 @@ function createProfileMethods(deps) {
802
802
  }
803
803
 
804
804
  // src/client/version-check.ts
805
- var SDK_VERSION = "2.19.0";
805
+ var SDK_VERSION = "2.21.0";
806
806
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
807
807
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
808
808
  function sdkHeaders() {
@@ -1796,6 +1796,63 @@ function createOtpMethods(deps) {
1796
1796
  };
1797
1797
  }
1798
1798
 
1799
+ // src/client/analytics-methods.ts
1800
+ var EXT_ANALYTICS_CONFIG_ENDPOINT = "/api/v1/ext/analytics/config";
1801
+ function resolveAuthOverride2(options, createError) {
1802
+ if (!options || options.accessToken === void 0) {
1803
+ return {};
1804
+ }
1805
+ const token = options.accessToken;
1806
+ if (typeof token !== "string" || token.trim() === "") {
1807
+ return {
1808
+ error: createError(
1809
+ "VALIDATION_ERROR",
1810
+ "accessToken \u3092\u6307\u5B9A\u3059\u308B\u5834\u5408\u3001\u7A7A\u6587\u5B57\u5217\u3084\u7A7A\u767D\u306E\u307F\u306E\u5024\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093"
1811
+ )
1812
+ };
1813
+ }
1814
+ return { override: { accessToken: token } };
1815
+ }
1816
+ var ANALYTICS_SERVICE_CODE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1817
+ function validateServiceCode(serviceCode, createError) {
1818
+ if (typeof serviceCode !== "string" || serviceCode.trim() === "") {
1819
+ return createError(
1820
+ "VALIDATION_ERROR",
1821
+ "serviceCode \u306F\u5FC5\u9808\u306E kebab-case \u6587\u5B57\u5217\u3067\u3059"
1822
+ );
1823
+ }
1824
+ if (!ANALYTICS_SERVICE_CODE_PATTERN.test(serviceCode)) {
1825
+ return createError(
1826
+ "VALIDATION_ERROR",
1827
+ "serviceCode \u306F kebab-case \u5F62\u5F0F (\u82F1\u5C0F\u6587\u5B57\u30FB\u6570\u5B57\u30FB\u30CF\u30A4\u30D5\u30F3) \u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
1828
+ );
1829
+ }
1830
+ return null;
1831
+ }
1832
+ function createAnalyticsMethods(deps) {
1833
+ const { fetchWithAuth, createError } = deps;
1834
+ async function getAnalyticsConfig(serviceCode, options) {
1835
+ const validationError = validateServiceCode(serviceCode, createError);
1836
+ if (validationError) {
1837
+ return { error: validationError };
1838
+ }
1839
+ const { override, error: overrideError } = resolveAuthOverride2(
1840
+ options,
1841
+ createError
1842
+ );
1843
+ if (overrideError) {
1844
+ return { error: overrideError };
1845
+ }
1846
+ const endpoint = `${EXT_ANALYTICS_CONFIG_ENDPOINT}?service=${encodeURIComponent(serviceCode)}`;
1847
+ return fetchWithAuth(
1848
+ endpoint,
1849
+ { method: "GET" },
1850
+ override
1851
+ );
1852
+ }
1853
+ return { getAnalyticsConfig };
1854
+ }
1855
+
1799
1856
  // src/client/contract-wizard-methods.ts
1800
1857
  var CONTRACT_WIZARD_PATH = "/contract-wizard";
1801
1858
  function buildWizardUrl(baseUrl, flow, params) {
@@ -2318,6 +2375,10 @@ function createFFIDClient(config) {
2318
2375
  fetchWithAuth,
2319
2376
  createError
2320
2377
  });
2378
+ const { getAnalyticsConfig } = createAnalyticsMethods({
2379
+ fetchWithAuth,
2380
+ createError
2381
+ });
2321
2382
  const {
2322
2383
  requestPasswordReset,
2323
2384
  verifyPasswordResetToken,
@@ -2391,6 +2452,7 @@ function createFFIDClient(config) {
2391
2452
  removeMember,
2392
2453
  getProfile,
2393
2454
  updateProfile,
2455
+ getAnalyticsConfig,
2394
2456
  createCheckoutSession,
2395
2457
  createPortalSession,
2396
2458
  listPlans,
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ // src/server/test-client.ts
4
+ var TEST_CLIENT_BRAND = /* @__PURE__ */ Symbol("@feelflow/ffid-sdk/test-client");
5
+ var TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK = "I-acknowledge-this-bypasses-real-auth";
6
+ function isTestFFIDClient(client) {
7
+ return typeof client === "object" && client !== null && TEST_CLIENT_BRAND in client && client[TEST_CLIENT_BRAND] === true;
8
+ }
9
+ var ERROR_CODE_TOKEN_VERIFICATION = "TOKEN_VERIFICATION_ERROR";
10
+ var PRODUCTION_REFUSAL_HINT = "If this is intentional (staging that mirrors production env), pass `allowInProduction: TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK` and ensure the bypass tokens are not reachable from customer traffic.";
11
+ function readNormalizedNodeEnv() {
12
+ if (typeof process === "undefined" || typeof process.env === "undefined") {
13
+ return void 0;
14
+ }
15
+ const raw = process.env.NODE_ENV;
16
+ return typeof raw === "string" ? raw.trim().toLowerCase() : void 0;
17
+ }
18
+ function emitProductionAcknowledgmentWarning() {
19
+ const msg = "[FFID] createTestFFIDClient: allowInProduction is set with NODE_ENV=production. Bypass tokens authenticate real requests in this process. Confirm this is staging.";
20
+ if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
21
+ process.emitWarning(msg, "FFIDTestModeInProduction");
22
+ } else {
23
+ console.warn(msg);
24
+ }
25
+ }
26
+ function assertSafeEnvironment(acknowledged) {
27
+ const hasProcessEnv = typeof process !== "undefined" && typeof process.env !== "undefined";
28
+ const nodeEnv = readNormalizedNodeEnv();
29
+ const isProduction = nodeEnv === "production";
30
+ if (acknowledged) {
31
+ if (isProduction) emitProductionAcknowledgmentWarning();
32
+ return;
33
+ }
34
+ if (!hasProcessEnv) {
35
+ throw new Error(
36
+ "createTestFFIDClient: refused to start because the runtime does not expose process.env (likely an Edge / Cloudflare Workers / browser runtime). NODE_ENV cannot be verified here. " + PRODUCTION_REFUSAL_HINT
37
+ );
38
+ }
39
+ if (isProduction) {
40
+ throw new Error(
41
+ "createTestFFIDClient: refused to start because NODE_ENV=production. " + PRODUCTION_REFUSAL_HINT
42
+ );
43
+ }
44
+ }
45
+ function assertValidConfig(config) {
46
+ if (!config.users || config.users.length === 0) {
47
+ throw new Error("createTestFFIDClient: `users` must contain at least one entry");
48
+ }
49
+ const seenTokens = /* @__PURE__ */ new Set();
50
+ for (const user of config.users) {
51
+ if (!user.bypassToken || !user.bypassToken.trim()) {
52
+ throw new Error("createTestFFIDClient: every user requires a non-empty bypassToken");
53
+ }
54
+ if (!user.userInfo || !user.userInfo.sub) {
55
+ throw new Error("createTestFFIDClient: every user requires userInfo.sub");
56
+ }
57
+ if (seenTokens.has(user.bypassToken)) {
58
+ throw new Error(
59
+ "createTestFFIDClient: duplicate bypassToken detected (each token must map to exactly one user)"
60
+ );
61
+ }
62
+ seenTokens.add(user.bypassToken);
63
+ }
64
+ }
65
+ function createTestVerifyAccessToken(config) {
66
+ const acknowledged = config.allowInProduction === TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK;
67
+ assertSafeEnvironment(acknowledged);
68
+ assertValidConfig(config);
69
+ const lookup = /* @__PURE__ */ new Map();
70
+ for (const user of config.users) {
71
+ lookup.set(user.bypassToken, { ...user.userInfo });
72
+ }
73
+ return async function verifyAccessToken(accessToken) {
74
+ if (!accessToken || !accessToken.trim()) {
75
+ return {
76
+ error: {
77
+ code: ERROR_CODE_TOKEN_VERIFICATION,
78
+ message: "\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
79
+ }
80
+ };
81
+ }
82
+ const matched = lookup.get(accessToken);
83
+ if (!matched) {
84
+ return {
85
+ error: {
86
+ code: ERROR_CODE_TOKEN_VERIFICATION,
87
+ message: "Test mode: bearer token did not match any registered bypass token"
88
+ }
89
+ };
90
+ }
91
+ return { data: { ...matched } };
92
+ };
93
+ }
94
+ function createTestFFIDClient(config) {
95
+ const verifyAccessToken = createTestVerifyAccessToken(config);
96
+ return {
97
+ verifyAccessToken,
98
+ [TEST_CLIENT_BRAND]: true
99
+ };
100
+ }
101
+
102
+ exports.TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK = TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK;
103
+ exports.createTestFFIDClient = createTestFFIDClient;
104
+ exports.createTestVerifyAccessToken = createTestVerifyAccessToken;
105
+ exports.isTestFFIDClient = isTestFFIDClient;
@@ -0,0 +1,125 @@
1
+ import { g as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-DgJRU-YZ.cjs';
2
+
3
+ /**
4
+ * FFID SDK - Test mode client (E2E / integration test bypass)
5
+ *
6
+ * Provides a synthetic `verifyAccessToken` implementation that returns
7
+ * predetermined user info for known bypass tokens, **without ever calling
8
+ * the real FFID introspect endpoint**. Intended for consumer-side E2E
9
+ * tests where exercising the full OAuth flow is impractical (e.g.,
10
+ * automated browser tests against a staging environment).
11
+ *
12
+ * ⚠️ SECURITY: Construction is **refused** when `NODE_ENV` resolves to
13
+ * `production` (after trim + case-fold) or when the runtime does not
14
+ * expose `process.env` at all (e.g., Edge / Cloudflare Workers). The
15
+ * single escape hatch is the `allowInProduction` literal acknowledgment
16
+ * string. This is a defense-in-depth measure — never enable in
17
+ * customer-facing production.
18
+ *
19
+ * Recommended import path (sub-path keeps this out of production bundles):
20
+ *
21
+ * import { createTestFFIDClient } from '@feelflow/ffid-sdk/server/test'
22
+ */
23
+
24
+ /**
25
+ * Module-private symbol used to brand `TestFFIDClient` instances. Because
26
+ * the symbol is created here (not via `Symbol.for`), it cannot be obtained
27
+ * outside this module — which makes the brand unforgeable from foreign
28
+ * code. The brand still exists only for runtime narrowing convenience and
29
+ * is **not a security boundary**: enforcement of test-only behavior comes
30
+ * from the production guard, not from this brand.
31
+ */
32
+ declare const TEST_CLIENT_BRAND: unique symbol;
33
+ /**
34
+ * Acknowledgment string a caller must pass to `allowInProduction` when
35
+ * intentionally constructing the test client in a `NODE_ENV=production`
36
+ * runtime (typical for staging that mirrors production env). The literal
37
+ * value is grep-able so audits can find every legitimate use.
38
+ */
39
+ declare const TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK: "I-acknowledge-this-bypasses-real-auth";
40
+ type AllowInProductionAck = typeof TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK;
41
+ /**
42
+ * A synthetic user that the test client recognises.
43
+ *
44
+ * `bypassToken` is the Bearer token a test caller sends in `Authorization`;
45
+ * the remaining fields populate the `FFIDOAuthUserInfo` returned by
46
+ * `verifyAccessToken`. Use distinct, non-trivial bypass tokens (≥ 32 chars
47
+ * recommended) — they grant full session impersonation when leaked.
48
+ */
49
+ interface TestFFIDUser {
50
+ /** Bearer token value that resolves to this user */
51
+ bypassToken: string;
52
+ /** Synthetic FFID OAuth userinfo returned for matching bypass tokens */
53
+ userInfo: FFIDOAuthUserInfo;
54
+ }
55
+ /**
56
+ * Configuration accepted by `createTestFFIDClient` / `createTestVerifyAccessToken`.
57
+ */
58
+ interface TestFFIDClientConfig {
59
+ /**
60
+ * Synthetic users keyed by `bypassToken`. Must contain at least one
61
+ * entry — the type encodes the non-empty rule via tuple syntax.
62
+ */
63
+ users: readonly [TestFFIDUser, ...TestFFIDUser[]];
64
+ /**
65
+ * Escape hatch to allow test-mode usage when `NODE_ENV` resolves to
66
+ * `production` (or when the runtime does not expose `process.env`).
67
+ *
68
+ * Must be set to the exact literal {@link TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK}
69
+ * — a `boolean` is intentionally rejected at the type level so a stray
70
+ * `allowInProduction: someFlag` cannot slip through code review. When
71
+ * honored in a production runtime, a runtime warning is emitted via
72
+ * `process.emitWarning('...', 'FFIDTestModeInProduction')`.
73
+ */
74
+ allowInProduction?: AllowInProductionAck;
75
+ }
76
+ /**
77
+ * Narrow client surface returned by `createTestFFIDClient`.
78
+ *
79
+ * Only `verifyAccessToken` is implemented — other `FFIDClient` methods
80
+ * would either need network access or have no meaningful test stub. The
81
+ * `verifyAccessToken` signature is bound to `FFIDClient['verifyAccessToken']`
82
+ * so any future production change (e.g., new options) becomes a TypeScript
83
+ * error here, surfacing drift instead of letting the test client silently
84
+ * lag the real client.
85
+ */
86
+ interface TestFFIDClient {
87
+ verifyAccessToken: FFIDClient['verifyAccessToken'];
88
+ /** Module-private symbol used by {@link isTestFFIDClient} for runtime narrowing */
89
+ readonly [TEST_CLIENT_BRAND]: true;
90
+ }
91
+ /**
92
+ * Returns true when the given client was produced by `createTestFFIDClient`.
93
+ *
94
+ * Intended as a **runtime-narrowing** helper for code holding a
95
+ * `FFIDClient | TestFFIDClient` union. The brand symbol is module-private
96
+ * and unforgeable from outside this module, but `isTestFFIDClient` is not
97
+ * a trust boundary — production code paths should rely on the construction-
98
+ * time `NODE_ENV` guard, not on this check.
99
+ */
100
+ declare function isTestFFIDClient(client: unknown): client is TestFFIDClient;
101
+ /**
102
+ * Build a `verifyAccessToken` function that bypasses the real FFID
103
+ * introspect call and returns synthetic user info for known tokens.
104
+ *
105
+ * @throws {Error} when `NODE_ENV` resolves to `production` and
106
+ * `allowInProduction` is not set, when `process.env` is unavailable
107
+ * (Edge / Workers) and `allowInProduction` is not set, or when
108
+ * `config` fails validation (empty users / blank or duplicate
109
+ * bypassToken / missing userInfo.sub).
110
+ */
111
+ declare function createTestVerifyAccessToken(config: TestFFIDClientConfig): TestFFIDClient['verifyAccessToken'];
112
+ /**
113
+ * Create a minimal FFID client suitable for E2E tests.
114
+ *
115
+ * Returns an object exposing only `verifyAccessToken`; other methods that
116
+ * would normally call the real FFID API are intentionally **not** stubbed
117
+ * to avoid silent fallbacks. Callers that need additional methods should
118
+ * mock those at the test boundary (e.g., MSW for HTTP) rather than
119
+ * extending this client.
120
+ *
121
+ * @throws {Error} same conditions as {@link createTestVerifyAccessToken}.
122
+ */
123
+ declare function createTestFFIDClient(config: TestFFIDClientConfig): TestFFIDClient;
124
+
125
+ export { TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK, type TestFFIDClient, type TestFFIDClientConfig, type TestFFIDUser, createTestFFIDClient, createTestVerifyAccessToken, isTestFFIDClient };
@@ -0,0 +1,125 @@
1
+ import { g as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-DgJRU-YZ.js';
2
+
3
+ /**
4
+ * FFID SDK - Test mode client (E2E / integration test bypass)
5
+ *
6
+ * Provides a synthetic `verifyAccessToken` implementation that returns
7
+ * predetermined user info for known bypass tokens, **without ever calling
8
+ * the real FFID introspect endpoint**. Intended for consumer-side E2E
9
+ * tests where exercising the full OAuth flow is impractical (e.g.,
10
+ * automated browser tests against a staging environment).
11
+ *
12
+ * ⚠️ SECURITY: Construction is **refused** when `NODE_ENV` resolves to
13
+ * `production` (after trim + case-fold) or when the runtime does not
14
+ * expose `process.env` at all (e.g., Edge / Cloudflare Workers). The
15
+ * single escape hatch is the `allowInProduction` literal acknowledgment
16
+ * string. This is a defense-in-depth measure — never enable in
17
+ * customer-facing production.
18
+ *
19
+ * Recommended import path (sub-path keeps this out of production bundles):
20
+ *
21
+ * import { createTestFFIDClient } from '@feelflow/ffid-sdk/server/test'
22
+ */
23
+
24
+ /**
25
+ * Module-private symbol used to brand `TestFFIDClient` instances. Because
26
+ * the symbol is created here (not via `Symbol.for`), it cannot be obtained
27
+ * outside this module — which makes the brand unforgeable from foreign
28
+ * code. The brand still exists only for runtime narrowing convenience and
29
+ * is **not a security boundary**: enforcement of test-only behavior comes
30
+ * from the production guard, not from this brand.
31
+ */
32
+ declare const TEST_CLIENT_BRAND: unique symbol;
33
+ /**
34
+ * Acknowledgment string a caller must pass to `allowInProduction` when
35
+ * intentionally constructing the test client in a `NODE_ENV=production`
36
+ * runtime (typical for staging that mirrors production env). The literal
37
+ * value is grep-able so audits can find every legitimate use.
38
+ */
39
+ declare const TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK: "I-acknowledge-this-bypasses-real-auth";
40
+ type AllowInProductionAck = typeof TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK;
41
+ /**
42
+ * A synthetic user that the test client recognises.
43
+ *
44
+ * `bypassToken` is the Bearer token a test caller sends in `Authorization`;
45
+ * the remaining fields populate the `FFIDOAuthUserInfo` returned by
46
+ * `verifyAccessToken`. Use distinct, non-trivial bypass tokens (≥ 32 chars
47
+ * recommended) — they grant full session impersonation when leaked.
48
+ */
49
+ interface TestFFIDUser {
50
+ /** Bearer token value that resolves to this user */
51
+ bypassToken: string;
52
+ /** Synthetic FFID OAuth userinfo returned for matching bypass tokens */
53
+ userInfo: FFIDOAuthUserInfo;
54
+ }
55
+ /**
56
+ * Configuration accepted by `createTestFFIDClient` / `createTestVerifyAccessToken`.
57
+ */
58
+ interface TestFFIDClientConfig {
59
+ /**
60
+ * Synthetic users keyed by `bypassToken`. Must contain at least one
61
+ * entry — the type encodes the non-empty rule via tuple syntax.
62
+ */
63
+ users: readonly [TestFFIDUser, ...TestFFIDUser[]];
64
+ /**
65
+ * Escape hatch to allow test-mode usage when `NODE_ENV` resolves to
66
+ * `production` (or when the runtime does not expose `process.env`).
67
+ *
68
+ * Must be set to the exact literal {@link TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK}
69
+ * — a `boolean` is intentionally rejected at the type level so a stray
70
+ * `allowInProduction: someFlag` cannot slip through code review. When
71
+ * honored in a production runtime, a runtime warning is emitted via
72
+ * `process.emitWarning('...', 'FFIDTestModeInProduction')`.
73
+ */
74
+ allowInProduction?: AllowInProductionAck;
75
+ }
76
+ /**
77
+ * Narrow client surface returned by `createTestFFIDClient`.
78
+ *
79
+ * Only `verifyAccessToken` is implemented — other `FFIDClient` methods
80
+ * would either need network access or have no meaningful test stub. The
81
+ * `verifyAccessToken` signature is bound to `FFIDClient['verifyAccessToken']`
82
+ * so any future production change (e.g., new options) becomes a TypeScript
83
+ * error here, surfacing drift instead of letting the test client silently
84
+ * lag the real client.
85
+ */
86
+ interface TestFFIDClient {
87
+ verifyAccessToken: FFIDClient['verifyAccessToken'];
88
+ /** Module-private symbol used by {@link isTestFFIDClient} for runtime narrowing */
89
+ readonly [TEST_CLIENT_BRAND]: true;
90
+ }
91
+ /**
92
+ * Returns true when the given client was produced by `createTestFFIDClient`.
93
+ *
94
+ * Intended as a **runtime-narrowing** helper for code holding a
95
+ * `FFIDClient | TestFFIDClient` union. The brand symbol is module-private
96
+ * and unforgeable from outside this module, but `isTestFFIDClient` is not
97
+ * a trust boundary — production code paths should rely on the construction-
98
+ * time `NODE_ENV` guard, not on this check.
99
+ */
100
+ declare function isTestFFIDClient(client: unknown): client is TestFFIDClient;
101
+ /**
102
+ * Build a `verifyAccessToken` function that bypasses the real FFID
103
+ * introspect call and returns synthetic user info for known tokens.
104
+ *
105
+ * @throws {Error} when `NODE_ENV` resolves to `production` and
106
+ * `allowInProduction` is not set, when `process.env` is unavailable
107
+ * (Edge / Workers) and `allowInProduction` is not set, or when
108
+ * `config` fails validation (empty users / blank or duplicate
109
+ * bypassToken / missing userInfo.sub).
110
+ */
111
+ declare function createTestVerifyAccessToken(config: TestFFIDClientConfig): TestFFIDClient['verifyAccessToken'];
112
+ /**
113
+ * Create a minimal FFID client suitable for E2E tests.
114
+ *
115
+ * Returns an object exposing only `verifyAccessToken`; other methods that
116
+ * would normally call the real FFID API are intentionally **not** stubbed
117
+ * to avoid silent fallbacks. Callers that need additional methods should
118
+ * mock those at the test boundary (e.g., MSW for HTTP) rather than
119
+ * extending this client.
120
+ *
121
+ * @throws {Error} same conditions as {@link createTestVerifyAccessToken}.
122
+ */
123
+ declare function createTestFFIDClient(config: TestFFIDClientConfig): TestFFIDClient;
124
+
125
+ export { TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK, type TestFFIDClient, type TestFFIDClientConfig, type TestFFIDUser, createTestFFIDClient, createTestVerifyAccessToken, isTestFFIDClient };
@@ -0,0 +1,100 @@
1
+ // src/server/test-client.ts
2
+ var TEST_CLIENT_BRAND = /* @__PURE__ */ Symbol("@feelflow/ffid-sdk/test-client");
3
+ var TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK = "I-acknowledge-this-bypasses-real-auth";
4
+ function isTestFFIDClient(client) {
5
+ return typeof client === "object" && client !== null && TEST_CLIENT_BRAND in client && client[TEST_CLIENT_BRAND] === true;
6
+ }
7
+ var ERROR_CODE_TOKEN_VERIFICATION = "TOKEN_VERIFICATION_ERROR";
8
+ var PRODUCTION_REFUSAL_HINT = "If this is intentional (staging that mirrors production env), pass `allowInProduction: TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK` and ensure the bypass tokens are not reachable from customer traffic.";
9
+ function readNormalizedNodeEnv() {
10
+ if (typeof process === "undefined" || typeof process.env === "undefined") {
11
+ return void 0;
12
+ }
13
+ const raw = process.env.NODE_ENV;
14
+ return typeof raw === "string" ? raw.trim().toLowerCase() : void 0;
15
+ }
16
+ function emitProductionAcknowledgmentWarning() {
17
+ const msg = "[FFID] createTestFFIDClient: allowInProduction is set with NODE_ENV=production. Bypass tokens authenticate real requests in this process. Confirm this is staging.";
18
+ if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
19
+ process.emitWarning(msg, "FFIDTestModeInProduction");
20
+ } else {
21
+ console.warn(msg);
22
+ }
23
+ }
24
+ function assertSafeEnvironment(acknowledged) {
25
+ const hasProcessEnv = typeof process !== "undefined" && typeof process.env !== "undefined";
26
+ const nodeEnv = readNormalizedNodeEnv();
27
+ const isProduction = nodeEnv === "production";
28
+ if (acknowledged) {
29
+ if (isProduction) emitProductionAcknowledgmentWarning();
30
+ return;
31
+ }
32
+ if (!hasProcessEnv) {
33
+ throw new Error(
34
+ "createTestFFIDClient: refused to start because the runtime does not expose process.env (likely an Edge / Cloudflare Workers / browser runtime). NODE_ENV cannot be verified here. " + PRODUCTION_REFUSAL_HINT
35
+ );
36
+ }
37
+ if (isProduction) {
38
+ throw new Error(
39
+ "createTestFFIDClient: refused to start because NODE_ENV=production. " + PRODUCTION_REFUSAL_HINT
40
+ );
41
+ }
42
+ }
43
+ function assertValidConfig(config) {
44
+ if (!config.users || config.users.length === 0) {
45
+ throw new Error("createTestFFIDClient: `users` must contain at least one entry");
46
+ }
47
+ const seenTokens = /* @__PURE__ */ new Set();
48
+ for (const user of config.users) {
49
+ if (!user.bypassToken || !user.bypassToken.trim()) {
50
+ throw new Error("createTestFFIDClient: every user requires a non-empty bypassToken");
51
+ }
52
+ if (!user.userInfo || !user.userInfo.sub) {
53
+ throw new Error("createTestFFIDClient: every user requires userInfo.sub");
54
+ }
55
+ if (seenTokens.has(user.bypassToken)) {
56
+ throw new Error(
57
+ "createTestFFIDClient: duplicate bypassToken detected (each token must map to exactly one user)"
58
+ );
59
+ }
60
+ seenTokens.add(user.bypassToken);
61
+ }
62
+ }
63
+ function createTestVerifyAccessToken(config) {
64
+ const acknowledged = config.allowInProduction === TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK;
65
+ assertSafeEnvironment(acknowledged);
66
+ assertValidConfig(config);
67
+ const lookup = /* @__PURE__ */ new Map();
68
+ for (const user of config.users) {
69
+ lookup.set(user.bypassToken, { ...user.userInfo });
70
+ }
71
+ return async function verifyAccessToken(accessToken) {
72
+ if (!accessToken || !accessToken.trim()) {
73
+ return {
74
+ error: {
75
+ code: ERROR_CODE_TOKEN_VERIFICATION,
76
+ message: "\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093"
77
+ }
78
+ };
79
+ }
80
+ const matched = lookup.get(accessToken);
81
+ if (!matched) {
82
+ return {
83
+ error: {
84
+ code: ERROR_CODE_TOKEN_VERIFICATION,
85
+ message: "Test mode: bearer token did not match any registered bypass token"
86
+ }
87
+ };
88
+ }
89
+ return { data: { ...matched } };
90
+ };
91
+ }
92
+ function createTestFFIDClient(config) {
93
+ const verifyAccessToken = createTestVerifyAccessToken(config);
94
+ return {
95
+ verifyAccessToken,
96
+ [TEST_CLIENT_BRAND]: true
97
+ };
98
+ }
99
+
100
+ export { TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK, createTestFFIDClient, createTestVerifyAccessToken, isTestFFIDClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",
@@ -60,6 +60,11 @@
60
60
  "types": "./dist/server/index.d.ts",
61
61
  "import": "./dist/server/index.js",
62
62
  "require": "./dist/server/index.cjs"
63
+ },
64
+ "./server/test": {
65
+ "types": "./dist/server/test/index.d.ts",
66
+ "import": "./dist/server/test/index.js",
67
+ "require": "./dist/server/test/index.cjs"
63
68
  }
64
69
  },
65
70
  "files": [