@bamdra/bamdra-user-bind 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/index.js +203 -58
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -425,6 +425,7 @@ var UserBindRuntime = class {
425
425
  bindingCache = /* @__PURE__ */ new Map();
426
426
  feishuScopeStatus = null;
427
427
  bitableMirror = null;
428
+ feishuTokenCache = /* @__PURE__ */ new Map();
428
429
  close() {
429
430
  this.store.close();
430
431
  }
@@ -707,77 +708,162 @@ var UserBindRuntime = class {
707
708
  return identity;
708
709
  }
709
710
  async tryResolveFeishuUser(openId) {
710
- const executor = this.host.callTool ?? this.host.invokeTool;
711
- if (typeof executor !== "function") {
712
- logUserBindEvent("feishu-resolution-skipped", { reason: "tool-executor-unavailable" });
711
+ logUserBindEvent("feishu-resolution-start", { openId });
712
+ const accounts = readFeishuAccountsFromOpenClawConfig();
713
+ if (accounts.length === 0) {
714
+ logUserBindEvent("feishu-resolution-skipped", { reason: "no-feishu-accounts-configured" });
713
715
  return null;
714
716
  }
715
- try {
716
- logUserBindEvent("feishu-resolution-start", { openId });
717
- const result = await executor.call(this.host, "feishu_user_get", {
718
- user_id_type: "open_id",
719
- user_id: openId
720
- });
721
- const candidate = extractDeepString(result, [
722
- ["data", "user", "user_id"],
723
- ["user", "user_id"],
724
- ["data", "user_id"]
725
- ]);
726
- if (!candidate) {
727
- logUserBindEvent("feishu-resolution-empty", { openId });
728
- return null;
717
+ for (const account of accounts) {
718
+ try {
719
+ const token = await this.getFeishuAppAccessToken(account);
720
+ const result = await feishuJsonRequest(
721
+ account,
722
+ `/open-apis/contact/v3/users/${encodeURIComponent(openId)}?user_id_type=open_id`,
723
+ token
724
+ );
725
+ const candidate = extractDeepString(result, [
726
+ ["data", "user", "user_id"],
727
+ ["user", "user_id"],
728
+ ["data", "user_id"]
729
+ ]);
730
+ if (!candidate) {
731
+ continue;
732
+ }
733
+ logUserBindEvent("feishu-resolution-success", {
734
+ accountId: account.accountId,
735
+ openId,
736
+ userId: candidate
737
+ });
738
+ return {
739
+ userId: candidate,
740
+ source: `feishu-api:${account.accountId}`,
741
+ profilePatch: {
742
+ name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]]),
743
+ email: extractDeepString(result, [["data", "user", "email"], ["user", "email"]]),
744
+ avatar: extractDeepString(result, [["data", "user", "avatar", "avatar_origin"], ["user", "avatar", "avatar_origin"]])
745
+ }
746
+ };
747
+ } catch (error) {
748
+ const message = error instanceof Error ? error.message : String(error);
749
+ logUserBindEvent("feishu-resolution-attempt-failed", {
750
+ accountId: account.accountId,
751
+ openId,
752
+ message
753
+ });
729
754
  }
730
- logUserBindEvent("feishu-resolution-success", { openId, userId: candidate });
731
- return {
732
- userId: candidate,
733
- source: "feishu-api",
734
- profilePatch: {
735
- name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]]),
736
- email: extractDeepString(result, [["data", "user", "email"], ["user", "email"]]),
737
- avatar: extractDeepString(result, [["data", "user", "avatar", "avatar_origin"], ["user", "avatar", "avatar_origin"]])
755
+ }
756
+ const executor = this.host.callTool ?? this.host.invokeTool;
757
+ if (typeof executor === "function") {
758
+ try {
759
+ const result = await executor.call(this.host, "feishu_user_get", {
760
+ user_id_type: "open_id",
761
+ user_id: openId
762
+ });
763
+ const candidate = extractDeepString(result, [
764
+ ["data", "user", "user_id"],
765
+ ["user", "user_id"],
766
+ ["data", "user_id"]
767
+ ]);
768
+ if (candidate) {
769
+ return {
770
+ userId: candidate,
771
+ source: "feishu-tool-fallback",
772
+ profilePatch: {
773
+ name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]])
774
+ }
775
+ };
738
776
  }
739
- };
740
- } catch (error) {
741
- const message = error instanceof Error ? error.message : String(error);
742
- logUserBindEvent("feishu-resolution-failed", { openId, message });
743
- this.store.recordIssue("feishu-resolution", message);
744
- return null;
777
+ } catch {
778
+ }
745
779
  }
780
+ logUserBindEvent("feishu-resolution-empty", { openId });
781
+ return null;
746
782
  }
747
783
  async ensureFeishuScopeStatus() {
748
784
  if (this.feishuScopeStatus) {
749
785
  return this.feishuScopeStatus;
750
786
  }
787
+ const accounts = readFeishuAccountsFromOpenClawConfig();
788
+ for (const account of accounts) {
789
+ try {
790
+ const token = await this.getFeishuAppAccessToken(account);
791
+ const result = await feishuJsonRequest(
792
+ account,
793
+ "/open-apis/application/v6/scopes",
794
+ token
795
+ );
796
+ const scopes = extractScopes(result);
797
+ this.feishuScopeStatus = {
798
+ scopes,
799
+ missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
800
+ hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
801
+ };
802
+ logUserBindEvent("feishu-scopes-read", {
803
+ accountId: account.accountId,
804
+ ...this.feishuScopeStatus
805
+ });
806
+ return this.feishuScopeStatus;
807
+ } catch (error) {
808
+ const message = error instanceof Error ? error.message : String(error);
809
+ logUserBindEvent("feishu-scopes-attempt-failed", { accountId: account.accountId, message });
810
+ }
811
+ }
751
812
  const executor = this.host.callTool ?? this.host.invokeTool;
752
- if (typeof executor !== "function") {
753
- this.feishuScopeStatus = {
754
- scopes: [],
755
- missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
756
- hasDocumentAccess: false
757
- };
758
- return this.feishuScopeStatus;
813
+ if (typeof executor === "function") {
814
+ try {
815
+ const result = await executor.call(this.host, "feishu_app_scopes", {});
816
+ const scopes = extractScopes(result);
817
+ this.feishuScopeStatus = {
818
+ scopes,
819
+ missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
820
+ hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
821
+ };
822
+ logUserBindEvent("feishu-scopes-read", this.feishuScopeStatus);
823
+ return this.feishuScopeStatus;
824
+ } catch (error) {
825
+ const message = error instanceof Error ? error.message : String(error);
826
+ logUserBindEvent("feishu-scopes-failed", { message });
827
+ this.store.recordIssue("feishu-scope-read", message);
828
+ }
759
829
  }
760
- try {
761
- const result = await executor.call(this.host, "feishu_app_scopes", {});
762
- const scopes = extractScopes(result);
763
- this.feishuScopeStatus = {
764
- scopes,
765
- missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
766
- hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
767
- };
768
- logUserBindEvent("feishu-scopes-read", this.feishuScopeStatus);
769
- return this.feishuScopeStatus;
770
- } catch (error) {
771
- const message = error instanceof Error ? error.message : String(error);
772
- logUserBindEvent("feishu-scopes-failed", { message });
773
- this.store.recordIssue("feishu-scope-read", message);
774
- this.feishuScopeStatus = {
775
- scopes: [],
776
- missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
777
- hasDocumentAccess: false
778
- };
779
- return this.feishuScopeStatus;
830
+ this.feishuScopeStatus = {
831
+ scopes: [],
832
+ missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
833
+ hasDocumentAccess: false
834
+ };
835
+ return this.feishuScopeStatus;
836
+ }
837
+ async getFeishuAppAccessToken(account) {
838
+ const cached = this.feishuTokenCache.get(account.accountId);
839
+ if (cached && cached.expiresAt > Date.now()) {
840
+ return cached.token;
841
+ }
842
+ const base = resolveFeishuOpenApiBase(account.domain);
843
+ const response = await fetch(`${base}/open-apis/auth/v3/app_access_token/internal`, {
844
+ method: "POST",
845
+ headers: {
846
+ "content-type": "application/json; charset=utf-8"
847
+ },
848
+ body: JSON.stringify({
849
+ app_id: account.appId,
850
+ app_secret: account.appSecret
851
+ })
852
+ });
853
+ const payload = await response.json();
854
+ if (!response.ok || Number(payload.code ?? 0) !== 0) {
855
+ throw new Error(`Failed to get Feishu app access token for ${account.accountId}: ${JSON.stringify(payload)}`);
780
856
  }
857
+ const token = asNullableString(payload.app_access_token);
858
+ if (!token) {
859
+ throw new Error(`Feishu app access token missing for ${account.accountId}`);
860
+ }
861
+ const expire = Number(payload.expire ?? 7200);
862
+ this.feishuTokenCache.set(account.accountId, {
863
+ token,
864
+ expiresAt: Date.now() + Math.max(60, expire - 120) * 1e3
865
+ });
866
+ return token;
781
867
  }
782
868
  async syncFeishuMirror(identity) {
783
869
  const scopeStatus = await this.ensureFeishuScopeStatus();
@@ -1445,6 +1531,65 @@ function findBitableRecordId(result, userId) {
1445
1531
  }
1446
1532
  return null;
1447
1533
  }
1534
+ function readFeishuAccountsFromOpenClawConfig() {
1535
+ const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
1536
+ if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
1537
+ return [];
1538
+ }
1539
+ try {
1540
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(openclawConfigPath, "utf8"));
1541
+ const channels = parsed.channels && typeof parsed.channels === "object" ? parsed.channels : {};
1542
+ const feishu = channels.feishu && typeof channels.feishu === "object" ? channels.feishu : {};
1543
+ const accounts = feishu.accounts && typeof feishu.accounts === "object" ? feishu.accounts : {};
1544
+ const topLevel = normalizeFeishuAccount("default", feishu, feishu);
1545
+ const values = Object.entries(accounts).map(([accountId, value]) => normalizeFeishuAccount(accountId, value, feishu)).filter((item) => item != null);
1546
+ if (topLevel && !values.some((item) => item.accountId === topLevel.accountId)) {
1547
+ values.unshift(topLevel);
1548
+ }
1549
+ return values;
1550
+ } catch (error) {
1551
+ logUserBindEvent("feishu-config-read-failed", {
1552
+ message: error instanceof Error ? error.message : String(error)
1553
+ });
1554
+ return [];
1555
+ }
1556
+ }
1557
+ function normalizeFeishuAccount(accountId, input, fallback) {
1558
+ const record = input && typeof input === "object" ? input : {};
1559
+ const enabled = record.enabled !== false && fallback.enabled !== false;
1560
+ const appId = asNullableString(record.appId) ?? asNullableString(fallback.appId);
1561
+ const appSecret = asNullableString(record.appSecret) ?? asNullableString(fallback.appSecret);
1562
+ const domain = asNullableString(record.domain) ?? asNullableString(fallback.domain) ?? "feishu";
1563
+ if (!enabled || !appId || !appSecret) {
1564
+ return null;
1565
+ }
1566
+ return { accountId, appId, appSecret, domain };
1567
+ }
1568
+ function resolveFeishuOpenApiBase(domain) {
1569
+ if (domain === "lark") {
1570
+ return "https://open.larksuite.com";
1571
+ }
1572
+ if (domain === "feishu") {
1573
+ return "https://open.feishu.cn";
1574
+ }
1575
+ return domain.replace(/\/+$/, "");
1576
+ }
1577
+ async function feishuJsonRequest(account, path, appAccessToken, init) {
1578
+ const base = resolveFeishuOpenApiBase(account.domain);
1579
+ const response = await fetch(`${base}${path}`, {
1580
+ ...init,
1581
+ headers: {
1582
+ authorization: `Bearer ${appAccessToken}`,
1583
+ "content-type": "application/json; charset=utf-8",
1584
+ ...init?.headers ?? {}
1585
+ }
1586
+ });
1587
+ const payload = await response.json();
1588
+ if (!response.ok || Number(payload.code ?? 0) !== 0) {
1589
+ throw new Error(JSON.stringify(payload));
1590
+ }
1591
+ return payload;
1592
+ }
1448
1593
  function getConfiguredAgentId(agent) {
1449
1594
  return asNullableString(agent.id) ?? asNullableString(agent.name) ?? asNullableString(agent.agentId);
1450
1595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamdra/bamdra-user-bind",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Identity resolution, user profile binding, and admin-safe profile tools for OpenClaw channels.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.bamdra.com",