@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 CHANGED
@@ -351,6 +351,43 @@ await client.updateProfile({
351
351
 
352
352
  対応フィールド: `displayName` / `phone` / `companyName` / `department` / `jobTitle` / `preferences`。`timezone` / `locale` は application-level invariant(サーバー側 normalization が string 前提)のため null 非許容。クリアは不可 — キー未指定で現状維持、もしくは新しい有効な値を渡す。
353
353
 
354
+ ### getAnalyticsConfig() (#2347)
355
+
356
+ 外部サービスが自身に割り当てられた GA4 Measurement ID を取得するメソッド。
357
+
358
+ - エンドポイント: `GET /api/v1/ext/analytics/config?service=<code>`
359
+ - 必要 scope: `analytics:read`(Bearer / API Key 両方で enforce)
360
+ - レスポンス: `FFIDAnalyticsConfig` (`{ code, measurementId, displayName, isActive }`)
361
+ - **archived service** (`isActive: false`) でも 200 を返す — caller が次回 deploy で tracking 停止判断する間、in-flight events が 5xx を起こさないように measurementId は引き続き返す
362
+
363
+ ```ts
364
+ import { createFFIDClient } from '@feelflow/ffid-sdk/server'
365
+
366
+ const client = createFFIDClient({
367
+ serviceCode: 'flow-board-ai',
368
+ authMode: 'service-key',
369
+ serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
370
+ })
371
+
372
+ const result = await client.getAnalyticsConfig('feel-agent-ai')
373
+ if (result.error) {
374
+ if (result.error.code === 'SERVICE_NOT_FOUND') {
375
+ // 該当 GA4 stream がまだ sync されていない / typo
376
+ } else {
377
+ console.error(`[FFID] analytics config fetch failed: ${result.error.code}`)
378
+ }
379
+ } else if (result.data.isActive) {
380
+ // GA4 タグを描画して events を送信
381
+ loadGA4Script(result.data.measurementId)
382
+ }
383
+ ```
384
+
385
+ エラーコード:
386
+ - `VALIDATION_ERROR` — `serviceCode` が空 / kebab-case 形式違反(SDK 側 pre-validate)
387
+ - `INSUFFICIENT_SCOPE` (403) — `analytics:read` scope なし
388
+ - `SERVICE_NOT_FOUND` (404) — DB に未登録の service code
389
+ - `INVALID_PARAM` (400) — server 側で形式違反を検出(pre-validate 通過後の boundary)
390
+
354
391
  ## 型定義
355
392
 
356
393
  ```typescript
@@ -487,6 +524,88 @@ SDK の `package.json` で `react` / `react-dom` は `optional: true` に設定
487
524
  }
488
525
  ```
489
526
 
527
+ ## E2E テストモード(`@feelflow/ffid-sdk/server/test`)
528
+
529
+ > **⚠️ SECURITY NOTICE** — テストモードは Bearer トークン検証を **意図的にバイパス** する仕組みです。本番環境で誤って有効化すると、登録されたバイパストークンを持つ任意のリクエストが認証通過します。
530
+
531
+ 各サービスで E2E テストを書く際、`verifyAccessToken` の introspect 呼び出しをモックする実装を独自に持つ必要がなくなります。SDK 側で **多重 production guard 付き** の bypass クライアントを提供します。
532
+
533
+ ```ts
534
+ import { createFFIDClient } from '@feelflow/ffid-sdk/server'
535
+ import { createTestFFIDClient } from '@feelflow/ffid-sdk/server/test'
536
+
537
+ const isE2E =
538
+ process.env.NODE_ENV !== 'production' &&
539
+ process.env.FFID_TEST_MODE === 'true'
540
+
541
+ const client = isE2E
542
+ ? createTestFFIDClient({
543
+ users: [
544
+ {
545
+ bypassToken: process.env.E2E_TEST_BYPASS_SECRET!,
546
+ userInfo: {
547
+ sub: 'e2e-test-sub',
548
+ email: 'e2e@example.com',
549
+ name: 'E2E Test User',
550
+ picture: null,
551
+ },
552
+ },
553
+ ],
554
+ })
555
+ : createFFIDClient({ /* normal production options */ })
556
+
557
+ const result = await client.verifyAccessToken(bearerToken)
558
+ ```
559
+
560
+ ### Built-in production guards (defense-in-depth)
561
+
562
+ - `NODE_ENV` を **trim + lowercase** 後に比較。`"production "`(改行混入)や `"Production"` も production として扱う(Vercel 環境変数の copy/paste 事故対策)
563
+ - `process.env` を露出しない runtime(Edge / Cloudflare Workers / browser)では **fail-close** で構築拒否
564
+ - `bypassToken` の重複検知 / 空チェック / `userInfo.sub` 必須チェック(構築時 throw)
565
+ - 未登録 token は **fail-close**(実 introspect への暗黙 fallthrough は行わない)
566
+ - 構築時点で `users` のスナップショットを取り、入力配列の post-construction mutation は無視
567
+ - 各 `verifyAccessToken` 呼び出しは新しいオブジェクトを返却(caller mutation が後続呼び出しを汚染しない)
568
+
569
+ ### `allowInProduction` escape hatch
570
+
571
+ staging が `NODE_ENV=production` をミラーするケース等のみ、明示的な ack 文字列で有効化できます:
572
+
573
+ ```ts
574
+ import {
575
+ createTestFFIDClient,
576
+ TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
577
+ } from '@feelflow/ffid-sdk/server/test'
578
+
579
+ createTestFFIDClient({
580
+ users: [...],
581
+ allowInProduction: TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
582
+ })
583
+ // → process.emitWarning(..., 'FFIDTestModeInProduction') を毎構築時に発火
584
+ ```
585
+
586
+ `boolean` ではなく **literal string ack** を要求する型なので、`allowInProduction: someBooleanFlag` のような誤代入はコンパイルエラーになります。ack 文字列は grep 可能で監査も容易です。
587
+
588
+ ### サブパス分離
589
+
590
+ `createTestFFIDClient` は **`@feelflow/ffid-sdk/server/test` からのみ** import 可能です(`@feelflow/ffid-sdk/server` にも main entry にも含まれない)。本番コードからの誤 import は ESLint の `no-restricted-imports` 等で検知することを推奨します:
591
+
592
+ ```js
593
+ // eslint.config.mjs (flat config)
594
+ import { defineConfig } from 'eslint/config'
595
+
596
+ export default defineConfig([
597
+ {
598
+ files: ['src/**/*.{ts,tsx}'], // production code only
599
+ ignores: ['**/__tests__/**', 'tests/e2e/**'],
600
+ rules: {
601
+ 'no-restricted-imports': ['error', {
602
+ patterns: ['@feelflow/ffid-sdk/server/test'],
603
+ }],
604
+ },
605
+ },
606
+ ])
607
+ ```
608
+
490
609
  ## 環境変数
491
610
 
492
611
  オプションで環境変数を使用してデフォルト設定を上書きできます:
@@ -807,7 +807,7 @@ function createProfileMethods(deps) {
807
807
  }
808
808
 
809
809
  // src/client/version-check.ts
810
- var SDK_VERSION = "2.19.0";
810
+ var SDK_VERSION = "2.21.0";
811
811
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
812
812
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
813
813
  function sdkHeaders() {
@@ -1840,6 +1840,63 @@ function createOtpMethods(deps) {
1840
1840
  };
1841
1841
  }
1842
1842
 
1843
+ // src/client/analytics-methods.ts
1844
+ var EXT_ANALYTICS_CONFIG_ENDPOINT = "/api/v1/ext/analytics/config";
1845
+ function resolveAuthOverride2(options, createError) {
1846
+ if (!options || options.accessToken === void 0) {
1847
+ return {};
1848
+ }
1849
+ const token = options.accessToken;
1850
+ if (typeof token !== "string" || token.trim() === "") {
1851
+ return {
1852
+ error: createError(
1853
+ "VALIDATION_ERROR",
1854
+ "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"
1855
+ )
1856
+ };
1857
+ }
1858
+ return { override: { accessToken: token } };
1859
+ }
1860
+ var ANALYTICS_SERVICE_CODE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1861
+ function validateServiceCode(serviceCode, createError) {
1862
+ if (typeof serviceCode !== "string" || serviceCode.trim() === "") {
1863
+ return createError(
1864
+ "VALIDATION_ERROR",
1865
+ "serviceCode \u306F\u5FC5\u9808\u306E kebab-case \u6587\u5B57\u5217\u3067\u3059"
1866
+ );
1867
+ }
1868
+ if (!ANALYTICS_SERVICE_CODE_PATTERN.test(serviceCode)) {
1869
+ return createError(
1870
+ "VALIDATION_ERROR",
1871
+ "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"
1872
+ );
1873
+ }
1874
+ return null;
1875
+ }
1876
+ function createAnalyticsMethods(deps) {
1877
+ const { fetchWithAuth, createError } = deps;
1878
+ async function getAnalyticsConfig(serviceCode, options) {
1879
+ const validationError = validateServiceCode(serviceCode, createError);
1880
+ if (validationError) {
1881
+ return { error: validationError };
1882
+ }
1883
+ const { override, error: overrideError } = resolveAuthOverride2(
1884
+ options,
1885
+ createError
1886
+ );
1887
+ if (overrideError) {
1888
+ return { error: overrideError };
1889
+ }
1890
+ const endpoint = `${EXT_ANALYTICS_CONFIG_ENDPOINT}?service=${encodeURIComponent(serviceCode)}`;
1891
+ return fetchWithAuth(
1892
+ endpoint,
1893
+ { method: "GET" },
1894
+ override
1895
+ );
1896
+ }
1897
+ return { getAnalyticsConfig };
1898
+ }
1899
+
1843
1900
  // src/client/contract-wizard-methods.ts
1844
1901
  var CONTRACT_WIZARD_PATH = "/contract-wizard";
1845
1902
  function buildWizardUrl(baseUrl, flow, params) {
@@ -2362,6 +2419,10 @@ function createFFIDClient(config) {
2362
2419
  fetchWithAuth,
2363
2420
  createError
2364
2421
  });
2422
+ const { getAnalyticsConfig } = createAnalyticsMethods({
2423
+ fetchWithAuth,
2424
+ createError
2425
+ });
2365
2426
  const {
2366
2427
  requestPasswordReset,
2367
2428
  verifyPasswordResetToken,
@@ -2435,6 +2496,7 @@ function createFFIDClient(config) {
2435
2496
  removeMember,
2436
2497
  getProfile,
2437
2498
  updateProfile,
2499
+ getAnalyticsConfig,
2438
2500
  createCheckoutSession,
2439
2501
  createPortalSession,
2440
2502
  listPlans,
@@ -805,7 +805,7 @@ function createProfileMethods(deps) {
805
805
  }
806
806
 
807
807
  // src/client/version-check.ts
808
- var SDK_VERSION = "2.19.0";
808
+ var SDK_VERSION = "2.21.0";
809
809
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
810
810
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
811
811
  function sdkHeaders() {
@@ -1838,6 +1838,63 @@ function createOtpMethods(deps) {
1838
1838
  };
1839
1839
  }
1840
1840
 
1841
+ // src/client/analytics-methods.ts
1842
+ var EXT_ANALYTICS_CONFIG_ENDPOINT = "/api/v1/ext/analytics/config";
1843
+ function resolveAuthOverride2(options, createError) {
1844
+ if (!options || options.accessToken === void 0) {
1845
+ return {};
1846
+ }
1847
+ const token = options.accessToken;
1848
+ if (typeof token !== "string" || token.trim() === "") {
1849
+ return {
1850
+ error: createError(
1851
+ "VALIDATION_ERROR",
1852
+ "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"
1853
+ )
1854
+ };
1855
+ }
1856
+ return { override: { accessToken: token } };
1857
+ }
1858
+ var ANALYTICS_SERVICE_CODE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1859
+ function validateServiceCode(serviceCode, createError) {
1860
+ if (typeof serviceCode !== "string" || serviceCode.trim() === "") {
1861
+ return createError(
1862
+ "VALIDATION_ERROR",
1863
+ "serviceCode \u306F\u5FC5\u9808\u306E kebab-case \u6587\u5B57\u5217\u3067\u3059"
1864
+ );
1865
+ }
1866
+ if (!ANALYTICS_SERVICE_CODE_PATTERN.test(serviceCode)) {
1867
+ return createError(
1868
+ "VALIDATION_ERROR",
1869
+ "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"
1870
+ );
1871
+ }
1872
+ return null;
1873
+ }
1874
+ function createAnalyticsMethods(deps) {
1875
+ const { fetchWithAuth, createError } = deps;
1876
+ async function getAnalyticsConfig(serviceCode, options) {
1877
+ const validationError = validateServiceCode(serviceCode, createError);
1878
+ if (validationError) {
1879
+ return { error: validationError };
1880
+ }
1881
+ const { override, error: overrideError } = resolveAuthOverride2(
1882
+ options,
1883
+ createError
1884
+ );
1885
+ if (overrideError) {
1886
+ return { error: overrideError };
1887
+ }
1888
+ const endpoint = `${EXT_ANALYTICS_CONFIG_ENDPOINT}?service=${encodeURIComponent(serviceCode)}`;
1889
+ return fetchWithAuth(
1890
+ endpoint,
1891
+ { method: "GET" },
1892
+ override
1893
+ );
1894
+ }
1895
+ return { getAnalyticsConfig };
1896
+ }
1897
+
1841
1898
  // src/client/contract-wizard-methods.ts
1842
1899
  var CONTRACT_WIZARD_PATH = "/contract-wizard";
1843
1900
  function buildWizardUrl(baseUrl, flow, params) {
@@ -2360,6 +2417,10 @@ function createFFIDClient(config) {
2360
2417
  fetchWithAuth,
2361
2418
  createError
2362
2419
  });
2420
+ const { getAnalyticsConfig } = createAnalyticsMethods({
2421
+ fetchWithAuth,
2422
+ createError
2423
+ });
2363
2424
  const {
2364
2425
  requestPasswordReset,
2365
2426
  verifyPasswordResetToken,
@@ -2433,6 +2494,7 @@ function createFFIDClient(config) {
2433
2494
  removeMember,
2434
2495
  getProfile,
2435
2496
  updateProfile,
2497
+ getAnalyticsConfig,
2436
2498
  createCheckoutSession,
2437
2499
  createPortalSession,
2438
2500
  listPlans,
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkBBXUZS4U_cjs = require('../chunk-BBXUZS4U.cjs');
3
+ var chunkHUU4Q5VH_cjs = require('../chunk-HUU4Q5VH.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunkBBXUZS4U_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunkBBXUZS4U_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunkBBXUZS4U_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunkBBXUZS4U_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunkBBXUZS4U_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunkBBXUZS4U_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunkBBXUZS4U_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkHUU4Q5VH_cjs.FFIDUserMenu; }
34
34
  });
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormOrganization, Z as FFIDInquiryFormPlaceholderContext, _ as FFIDInquiryFormPrefill, $ as FFIDInquiryFormProps, a0 as FFIDInquiryFormSubmitData, a1 as FFIDInquiryFormSubmitResult, a3 as FFIDLoginButton, ap as FFIDLoginButtonProps, a9 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-0D2vYSLq.cjs';
1
+ export { N as FFIDAnnouncementBadge, am as FFIDAnnouncementBadgeClassNames, an as FFIDAnnouncementBadgeProps, O as FFIDAnnouncementList, ao as FFIDAnnouncementListClassNames, ap as FFIDAnnouncementListProps, W as FFIDInquiryForm, X as FFIDInquiryFormCategoryItem, Y as FFIDInquiryFormClassNames, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, aq as FFIDLoginButtonProps, aa as FFIDOrganizationSwitcher, ar as FFIDOrganizationSwitcherClassNames, as as FFIDOrganizationSwitcherProps, ad as FFIDSubscriptionBadge, at as FFIDSubscriptionBadgeClassNames, au as FFIDSubscriptionBadgeProps, af as FFIDUserMenu, av as FFIDUserMenuClassNames, aw as FFIDUserMenuProps } from '../index-Dr5G9HQ4.cjs';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormOrganization, Z as FFIDInquiryFormPlaceholderContext, _ as FFIDInquiryFormPrefill, $ as FFIDInquiryFormProps, a0 as FFIDInquiryFormSubmitData, a1 as FFIDInquiryFormSubmitResult, a3 as FFIDLoginButton, ap as FFIDLoginButtonProps, a9 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-0D2vYSLq.js';
1
+ export { N as FFIDAnnouncementBadge, am as FFIDAnnouncementBadgeClassNames, an as FFIDAnnouncementBadgeProps, O as FFIDAnnouncementList, ao as FFIDAnnouncementListClassNames, ap as FFIDAnnouncementListProps, W as FFIDInquiryForm, X as FFIDInquiryFormCategoryItem, Y as FFIDInquiryFormClassNames, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, aq as FFIDLoginButtonProps, aa as FFIDOrganizationSwitcher, ar as FFIDOrganizationSwitcherClassNames, as as FFIDOrganizationSwitcherProps, ad as FFIDSubscriptionBadge, at as FFIDSubscriptionBadgeClassNames, au as FFIDSubscriptionBadgeProps, af as FFIDUserMenu, av as FFIDUserMenuClassNames, aw as FFIDUserMenuProps } from '../index-Dr5G9HQ4.js';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-SXYB5QM3.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-I7NEMG52.js';