@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.
- package/README.md +119 -0
- package/dist/{chunk-BBXUZS4U.cjs → chunk-HUU4Q5VH.cjs} +63 -1
- package/dist/{chunk-SXYB5QM3.js → chunk-I7NEMG52.js} +63 -1
- package/dist/components/index.cjs +8 -8
- package/dist/components/index.d.cts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/ffid-client-DgJRU-YZ.d.cts +1147 -0
- package/dist/ffid-client-DgJRU-YZ.d.ts +1147 -0
- package/dist/{index-0D2vYSLq.d.cts → index-Dr5G9HQ4.d.cts} +24 -1
- package/dist/{index-0D2vYSLq.d.ts → index-Dr5G9HQ4.d.ts} +24 -1
- package/dist/index.cjs +31 -31
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/server/index.cjs +63 -1
- package/dist/server/index.d.cts +3 -1123
- package/dist/server/index.d.ts +3 -1123
- package/dist/server/index.js +63 -1
- package/dist/server/test/index.cjs +105 -0
- package/dist/server/test/index.d.cts +125 -0
- package/dist/server/test/index.d.ts +125 -0
- package/dist/server/test/index.js +100 -0
- package/package.json +6 -1
package/dist/server/index.js
CHANGED
|
@@ -802,7 +802,7 @@ function createProfileMethods(deps) {
|
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
// src/client/version-check.ts
|
|
805
|
-
var SDK_VERSION = "2.
|
|
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.
|
|
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": [
|