@agent-team-foundation/first-tree-hub 0.10.0 → 0.10.2

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 (24) hide show
  1. package/dist/{bootstrap-CtVqQA8a.mjs → bootstrap-Ca5Fiqz6.mjs} +10 -1
  2. package/dist/cli/index.mjs +9 -19
  3. package/dist/{feishu-DEmwoNn_.mjs → dist-CLiN7cVS.mjs} +88 -51
  4. package/dist/drizzle/0026_saas_onboarding.sql +153 -0
  5. package/dist/drizzle/meta/_journal.json +7 -0
  6. package/dist/feishu-FTWnoOsc.mjs +52 -0
  7. package/dist/{getMachineId-bsd-BB-fnFLA.mjs → getMachineId-bsd-D0w3uAZa.mjs} +1 -1
  8. package/dist/{getMachineId-darwin-DAYWNsYK.mjs → getMachineId-darwin-DOoYFb2_.mjs} +1 -1
  9. package/dist/{getMachineId-win-H5RT49ov.mjs → getMachineId-win-B6hY8edq.mjs} +1 -1
  10. package/dist/index.mjs +7 -5
  11. package/dist/invitation-BTlGMy0o-dIoR8JRj.mjs +3 -0
  12. package/dist/invitation-C_zAhB8x-8Khychlu.mjs +258 -0
  13. package/dist/{observability-DDkJwSKv.mjs → observability-C08jUFsJ.mjs} +1 -1
  14. package/dist/{observability-DV_fQKqV-oxfXX6Z2.mjs → observability-DPyf745N-BSc8QNcR.mjs} +6 -6
  15. package/dist/{core-BgiFGT7Y.mjs → saas-connect-idjpoPTk.mjs} +1111 -153
  16. package/dist/web/assets/index-CEAPwdg7.js +377 -0
  17. package/dist/web/assets/index-CzWeWItA.css +1 -0
  18. package/dist/web/index.html +2 -2
  19. package/package.json +1 -1
  20. package/dist/web/assets/index-DStPeqrX.css +0 -1
  21. package/dist/web/assets/index-Wgxk3V_m.js +0 -371
  22. /package/dist/{execAsync-CP8iWV5b.mjs → execAsync-XMc-nFn-.mjs} +0 -0
  23. /package/dist/{getMachineId-linux-BU7Fi6S0.mjs → getMachineId-linux-MlY63Zsw.mjs} +0 -0
  24. /package/dist/{getMachineId-unsupported-BhWCxKBo.mjs → getMachineId-unsupported-BS652RIy.mjs} +0 -0
@@ -548,7 +548,8 @@ const serverConfigSchema = defineConfig({
548
548
  },
549
549
  server: {
550
550
  port: field(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
551
- host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
551
+ host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" }),
552
+ publicUrl: field(z.string().optional(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
552
553
  },
553
554
  secrets: {
554
555
  jwtSecret: field(z.string(), {
@@ -576,6 +577,14 @@ const serverConfigSchema = defineConfig({
576
577
  }),
577
578
  allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
578
579
  },
580
+ oauth: optional({ github: optional({
581
+ clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
582
+ clientSecret: field(z.string(), {
583
+ env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_SECRET",
584
+ secret: true
585
+ }),
586
+ devCallbackEnabled: field(z.boolean().default(false), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_DEV_CALLBACK" })
587
+ }) }),
579
588
  cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
580
589
  rateLimit: optional({
581
590
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
@@ -1,26 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import "../observability-DV_fQKqV-oxfXX6Z2.mjs";
3
- import { $ as configureClientLoggerForService, A as installClientService, B as createOwner, C as checkNodeVersion, D as checkWebSocket, E as checkServerReachable, G as setJsonMode, H as resolveReplyToFromEnv, I as stopPostgres, J as FirstTreeHubSDK, L as ClientRuntime, O as printResults, Q as applyClientLoggerConfig, R as handleClientOrgMismatch, S as checkDocker, T as checkServerHealth, W as print, X as SessionRegistry, Y as SdkError, Z as cleanWorkspaces, _ as runMigrations, a as COMMAND_VERSION, b as checkClientConfig, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as migrateLocalAgentDirs, h as createApiNameResolver, i as startServer, j as isServiceSupported, k as getClientServiceStatus, l as formatCheckReport, m as runHomeMigration, n as declineUpdate, o as isInteractive, p as saveOnboardState, q as ClientOrgMismatchError, r as promptUpdate, s as promptAddAgent, t as createExecuteUpdate, u as loadOnboardState, v as checkAgentConfigs, w as checkServerConfig, x as checkDatabase, y as checkBackgroundService } from "../core-BgiFGT7Y.mjs";
2
+ import "../observability-DPyf745N-BSc8QNcR.mjs";
3
+ import { $ as FirstTreeHubSDK, A as checkWebSocket, B as ClientRuntime, C as checkClientConfig, D as checkServerConfig, E as checkNodeVersion, G as resolveReplyToFromEnv, K as fail, M as getClientServiceStatus, N as installClientService, O as checkServerHealth, P as isServiceSupported, Q as ClientOrgMismatchError, S as checkBackgroundService, T as checkDocker, U as createOwner, V as handleClientOrgMismatch, X as setJsonMode, Y as print, _ as runHomeMigration, a as declineUpdate, b as runMigrations, c as COMMAND_VERSION, d as promptMissingFields, et as SdkError, f as formatCheckReport, g as saveOnboardState, h as onboardCreate, i as createExecuteUpdate, it as configureClientLoggerForService, j as printResults, k as checkServerReachable, l as isInteractive, m as onboardCheck, nt as cleanWorkspaces, o as promptUpdate, p as loadOnboardState, q as success, r as registerSaaSConnectCommand, rt as applyClientLoggerConfig, s as startServer, tt as SessionRegistry, u as promptAddAgent, v as createApiNameResolver, w as checkDatabase, x as checkAgentConfigs, y as migrateLocalAgentDirs, z as stopPostgres } from "../saas-connect-idjpoPTk.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-CtVqQA8a.mjs";
6
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DEmwoNn_.mjs";
5
+ import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-Ca5Fiqz6.mjs";
6
+ import "../dist-CLiN7cVS.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-FTWnoOsc.mjs";
8
+ import "../invitation-C_zAhB8x-8Khychlu.mjs";
7
9
  import { join } from "node:path";
8
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
9
11
  import { Command } from "commander";
10
12
  import { confirm, input, password, select } from "@inquirer/prompts";
11
- //#region src/cli/output.ts
12
- /**
13
- * CLI output re-exports. The underlying implementation lives in
14
- * `core/output.ts` (the Print layer). Keep these thin wrappers so callers that
15
- * only depend on `cli/output.ts` keep working during the migration.
16
- */
17
- function success(data) {
18
- print.result(data);
19
- }
20
- function fail(code, message, exitCode = 1) {
21
- return print.fail(code, message, exitCode);
22
- }
23
- //#endregion
24
13
  //#region src/commands/agent-config.ts
25
14
  async function resolveAgentRecord(serverUrl, adminToken, agentName) {
26
15
  const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
@@ -1223,13 +1212,13 @@ function isSecretField(schema, dotPath) {
1223
1212
  //#region src/commands/onboard.ts
1224
1213
  async function promptMissing(args) {
1225
1214
  if (!args.server) try {
1226
- const { resolveServerUrl } = await import("../bootstrap-CtVqQA8a.mjs").then((n) => n.t);
1215
+ const { resolveServerUrl } = await import("../bootstrap-Ca5Fiqz6.mjs").then((n) => n.t);
1227
1216
  resolveServerUrl();
1228
1217
  } catch {
1229
1218
  args.server = await input({ message: "Hub server URL:" });
1230
1219
  saveOnboardState(args);
1231
1220
  }
1232
- const { loadCredentials } = await import("../bootstrap-CtVqQA8a.mjs").then((n) => n.t);
1221
+ const { loadCredentials } = await import("../bootstrap-Ca5Fiqz6.mjs").then((n) => n.t);
1233
1222
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1234
1223
  if (!args.id) {
1235
1224
  args.id = await input({ message: "Agent ID:" });
@@ -1506,6 +1495,7 @@ program.name("first-tree-hub").description("First Tree Hub — centralized colla
1506
1495
  });
1507
1496
  else applyClientLoggerConfig({ level: "warn" });
1508
1497
  });
1498
+ registerSaaSConnectCommand(program);
1509
1499
  registerServerCommands(program);
1510
1500
  registerClientCommands(program);
1511
1501
  registerAgentCommands(program);
@@ -1,4 +1,3 @@
1
- import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
2
1
  import { z } from "zod";
3
2
  //#region ../shared/dist/index.mjs
4
3
  const MENTION_REGEX = /(?<![A-Za-z0-9_.@-])@([A-Za-z0-9][A-Za-z0-9_-]{0,63})\b/g;
@@ -38,6 +37,35 @@ function scanMentionTokens(content) {
38
37
  }
39
38
  return tokens;
40
39
  }
40
+ /**
41
+ * Single source of truth for "is this string safe to redirect to after a
42
+ * successful OAuth callback".
43
+ *
44
+ * Both the server (`/auth/github/start` validates `?next=` before signing
45
+ * the state JWT) and the web client (the fragment-consumer page validates
46
+ * before navigating) must agree on the regex — drift here is what enables
47
+ * open-redirect bugs. The server is authoritative; the client check is a
48
+ * defense-in-depth.
49
+ *
50
+ * Allowed: a path that begins with exactly one `/` and is not the start of
51
+ * an authority component (`//`, `/\`). Permits typical SPA paths with
52
+ * query strings and fragments. Anything else (absolute URLs, scheme-less
53
+ * authority components, `javascript:`) falls through to the safe default.
54
+ */
55
+ const SAFE_NEXT_PATH = /^\/(?![/\\])[A-Za-z0-9_\-./?=&%#]*$/;
56
+ /**
57
+ * Return `next` if it is a syntactically safe relative path, otherwise the
58
+ * default landing path. The check is deliberately conservative — the
59
+ * intent is to reject anything that could be parsed as an absolute URL by
60
+ * a browser navigation. Length is capped at 256 chars to defang
61
+ * pathological inputs.
62
+ */
63
+ function safeRedirectPath(next) {
64
+ if (!next || typeof next !== "string") return "/";
65
+ if (next.length > 256) return "/";
66
+ if (!SAFE_NEXT_PATH.test(next)) return "/";
67
+ return next;
68
+ }
41
69
  const adapterPlatformSchema = z.enum([
42
70
  "feishu",
43
71
  "slack",
@@ -667,6 +695,42 @@ z.object({
667
695
  ackedAt: z.string().nullable()
668
696
  }).extend({ message: clientMessageSchema });
669
697
  const inboxPollQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(50).default(10) });
698
+ z.object({
699
+ organizationId: z.string(),
700
+ organizationName: z.string(),
701
+ organizationDisplayName: z.string(),
702
+ role: z.string()
703
+ });
704
+ z.object({
705
+ id: z.string(),
706
+ organizationId: z.string(),
707
+ token: z.string(),
708
+ inviteUrl: z.string(),
709
+ role: z.string(),
710
+ createdAt: z.string(),
711
+ expiresAt: z.string().nullable()
712
+ });
713
+ /** Body for joining via invite token. */
714
+ const joinByInvitationSchema = z.object({ token: z.string().min(1) });
715
+ z.object({}).optional();
716
+ z.enum([
717
+ "connect",
718
+ "create_agent",
719
+ "completed"
720
+ ]);
721
+ z.object({
722
+ id: z.string(),
723
+ name: z.string(),
724
+ displayName: z.string(),
725
+ role: z.enum(["admin", "member"])
726
+ });
727
+ /** Body for `POST /me/organizations` — operator wants to create another team. */
728
+ const createOrgFromMeSchema = z.object({
729
+ name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/),
730
+ displayName: z.string().min(1).max(200)
731
+ });
732
+ /** Body for `POST /auth/switch-org`. */
733
+ const switchOrgSchema = z.object({ organizationId: z.string().min(1) });
670
734
  const memberRoleSchema = z.enum(["admin", "member"]);
671
735
  const memberSchema = z.object({
672
736
  id: z.string(),
@@ -724,6 +788,28 @@ const notificationQuerySchema = z.object({
724
788
  read: z.enum(["true", "false"]).transform((v) => v === "true").optional(),
725
789
  agentId: z.string().optional()
726
790
  });
791
+ /**
792
+ * `GET /api/v1/auth/github/start` query — `next` is the post-login landing
793
+ * path. It is validated again before signing the state JWT (see
794
+ * `safe-redirect.ts`); the schema only enforces the syntactic upper bound
795
+ * so over-long paths bounce with a Zod error rather than silently truncate.
796
+ */
797
+ const githubStartQuerySchema = z.object({ next: z.string().max(256).optional() });
798
+ const githubCallbackQuerySchema = z.object({
799
+ code: z.string().min(1),
800
+ state: z.string().min(1)
801
+ });
802
+ /**
803
+ * Dev-only callback to bypass the GitHub round-trip — sign in as a stub
804
+ * Github user. Gated by NODE_ENV !== 'production' AND `oauth.github.devCallbackEnabled`.
805
+ */
806
+ const githubDevCallbackQuerySchema = z.object({
807
+ githubId: z.string().min(1),
808
+ login: z.string().min(1),
809
+ email: z.string().email().optional(),
810
+ displayName: z.string().optional(),
811
+ next: z.string().max(256).optional()
812
+ });
727
813
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
728
814
  const createOrganizationSchema = z.object({
729
815
  name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Must start with a letter or digit and contain only lowercase alphanumeric and hyphens").refine((v) => !UUID_PATTERN.test(v), "Name must not be a UUID format"),
@@ -1061,53 +1147,4 @@ z.object({
1061
1147
  serverTimeMs: z.number().int().nonnegative()
1062
1148
  }).passthrough();
1063
1149
  //#endregion
1064
- //#region src/core/feishu.ts
1065
- var feishu_exports = /* @__PURE__ */ __exportAll({
1066
- bindFeishuBot: () => bindFeishuBot,
1067
- bindFeishuUser: () => bindFeishuUser
1068
- });
1069
- /**
1070
- * Feishu-related core operations: bind-bot, bind-user.
1071
- *
1072
- * All agent-scoped calls carry both the member access JWT (Authorization)
1073
- * and the acting agent UUID (X-Agent-Id); the server's agent-selector
1074
- * middleware enforces Rule R-RUN.
1075
- */
1076
- async function bindFeishuBot(serverUrl, accessToken, agentId, appId, appSecret) {
1077
- const res = await fetch(`${serverUrl}/api/v1/agent/me/feishu-bot`, {
1078
- method: "PUT",
1079
- headers: {
1080
- Authorization: `Bearer ${accessToken}`,
1081
- [AGENT_SELECTOR_HEADER]: agentId,
1082
- "Content-Type": "application/json"
1083
- },
1084
- body: JSON.stringify({
1085
- appId,
1086
- appSecret
1087
- })
1088
- });
1089
- if (!res.ok) {
1090
- const body = await res.json().catch(() => ({}));
1091
- throw new Error(body.error ?? `Bind Feishu bot failed: HTTP ${res.status}`);
1092
- }
1093
- }
1094
- async function bindFeishuUser(serverUrl, accessToken, agentId, humanAgentId, feishuUserId, displayName) {
1095
- const res = await fetch(`${serverUrl}/api/v1/agent/delegated/${encodeURIComponent(humanAgentId)}/feishu-user`, {
1096
- method: "POST",
1097
- headers: {
1098
- Authorization: `Bearer ${accessToken}`,
1099
- [AGENT_SELECTOR_HEADER]: agentId,
1100
- "Content-Type": "application/json"
1101
- },
1102
- body: JSON.stringify({
1103
- feishuUserId,
1104
- displayName
1105
- })
1106
- });
1107
- if (!res.ok) {
1108
- const body = await res.json().catch(() => ({}));
1109
- throw new Error(body.error ?? `Bind Feishu user failed: HTTP ${res.status}`);
1110
- }
1111
- }
1112
- //#endregion
1113
- export { sessionEventMessageSchema as $, createChatSchema as A, isReservedAgentName as B, agentRuntimeConfigPayloadSchema as C, createAdapterConfigSchema as D, connectTokenExchangeSchema as E, dryRunAgentRuntimeConfigSchema as F, paginationQuerySchema as G, loginSchema as H, extractMentions as I, scanMentionTokens as J, refreshTokenSchema as K, imageInlineContentSchema as L, createOrganizationSchema as M, createTaskSchema as N, createAdapterMappingSchema as O, delegateFeishuUserSchema as P, sessionCompletionMessageSchema as Q, inboxPollQuerySchema as R, agentPinnedMessageSchema as S, clientRegisterSchema as T, messageSourceSchema as U, linkTaskChatSchema as V, notificationQuerySchema as W, sendMessageSchema as X, selfServiceFeishuBotSchema as Y, sendToAgentSchema as Z, WS_AUTH_FRAME_TIMEOUT_MS as _, AGENT_NAME_REGEX as a, updateAgentRuntimeConfigSchema as at, adminUpdateTaskSchema as b, AGENT_STATUSES as c, updateMemberSchema as ct, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as d, updateTaskStatusSchema as dt, sessionEventSchema as et, SYSTEM_CONFIG_DEFAULTS as f, wsAuthFrameSchema as ft, TASK_TERMINAL_STATUSES as g, TASK_STATUSES as h, AGENT_BIND_REJECT_REASONS as i, updateAdapterConfigSchema as it, createMemberSchema as j, createAgentSchema as k, AGENT_TYPES as l, updateOrganizationSchema as lt, TASK_HEALTH_SIGNALS as m, bindFeishuUser as n, sessionStateMessageSchema as nt, AGENT_SELECTOR_HEADER as o, updateAgentSchema as ot, TASK_CREATOR_TYPES as p, runtimeStateMessageSchema as q, feishu_exports as r, taskListQuerySchema as rt, AGENT_SOURCES as s, updateChatSchema as st, bindFeishuBot as t, sessionReconcileRequestSchema as tt, AGENT_VISIBILITY as u, updateSystemConfigSchema as ut, addParticipantSchema as v, agentTypeSchema as w, agentBindRequestSchema as x, adminCreateTaskSchema as y, isRedactedEnvValue as z };
1150
+ export { sendMessageSchema as $, createOrganizationSchema as A, isRedactedEnvValue as B, connectTokenExchangeSchema as C, createChatSchema as D, createAgentSchema as E, githubCallbackQuerySchema as F, messageSourceSchema as G, joinByInvitationSchema as H, githubDevCallbackQuerySchema as I, refreshTokenSchema as J, notificationQuerySchema as K, githubStartQuerySchema as L, delegateFeishuUserSchema as M, dryRunAgentRuntimeConfigSchema as N, createMemberSchema as O, extractMentions as P, selfServiceFeishuBotSchema as Q, imageInlineContentSchema as R, clientRegisterSchema as S, createAdapterMappingSchema as T, linkTaskChatSchema as U, isReservedAgentName as V, loginSchema as W, safeRedirectPath as X, runtimeStateMessageSchema as Y, scanMentionTokens as Z, adminUpdateTaskSchema as _, AGENT_STATUSES as a, sessionStateMessageSchema as at, agentRuntimeConfigPayloadSchema as b, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as c, updateAdapterConfigSchema as ct, TASK_HEALTH_SIGNALS as d, updateChatSchema as dt, sendToAgentSchema as et, TASK_STATUSES as f, updateMemberSchema as ft, adminCreateTaskSchema as g, wsAuthFrameSchema as gt, addParticipantSchema as h, updateTaskStatusSchema as ht, AGENT_SOURCES as i, sessionReconcileRequestSchema as it, createTaskSchema as j, createOrgFromMeSchema as k, SYSTEM_CONFIG_DEFAULTS as l, updateAgentRuntimeConfigSchema as lt, WS_AUTH_FRAME_TIMEOUT_MS as m, updateSystemConfigSchema as mt, AGENT_NAME_REGEX as n, sessionEventMessageSchema as nt, AGENT_TYPES as o, switchOrgSchema as ot, TASK_TERMINAL_STATUSES as p, updateOrganizationSchema as pt, paginationQuerySchema as q, AGENT_SELECTOR_HEADER as r, sessionEventSchema as rt, AGENT_VISIBILITY as s, taskListQuerySchema as st, AGENT_BIND_REJECT_REASONS as t, sessionCompletionMessageSchema as tt, TASK_CREATOR_TYPES as u, updateAgentSchema as ut, agentBindRequestSchema as v, createAdapterConfigSchema as w, agentTypeSchema as x, agentPinnedMessageSchema as y, inboxPollQuerySchema as z };
@@ -0,0 +1,153 @@
1
+ -- SaaS onboarding milestone — adds the data-model surface needed for
2
+ -- public GitHub-OAuth signup, per-org invitation links, and "leave team"
3
+ -- soft-delete. See proposals/hub-saas-onboarding.20260428.md for the full
4
+ -- design contract.
5
+ --
6
+ -- Three independent additions, no destructive changes to existing tables:
7
+ --
8
+ -- 1. `auth_identities` — third-party / local auth identities for a user.
9
+ -- Models the "how does this user prove who they are" boundary.
10
+ -- `(provider, identifier)` is globally unique. v1 stores the credential
11
+ -- payload (password hash, webauthn pubkey) on the same row;
12
+ -- v2 splits it into `auth_credentials` if multi-factor is needed
13
+ -- (the migration is sketched in the schema file's header comment).
14
+ --
15
+ -- 2. `invitations` + `invitation_redemptions` — org-level share links.
16
+ -- The "one active link per org" rule is enforced by a partial UNIQUE
17
+ -- index (Drizzle's TS DSL doesn't model partial uniques yet, so we
18
+ -- add it directly here). Rotation = revoke prior + insert new in a
19
+ -- single transaction. Redemptions are recorded for audit.
20
+ --
21
+ -- 3. `members.status` — "active" | "left" soft-delete marker for the
22
+ -- "leave team" flow. Existing rows backfill to "active" via the
23
+ -- column DEFAULT. The auth middleware rejects tokens that resolve to
24
+ -- a "left" member; join-by-invite flips a "left" row back to "active".
25
+ --
26
+ -- All three changes are append-only (new tables + new column with DEFAULT).
27
+ -- ALTER TABLE on `members` takes a brief ACCESS EXCLUSIVE lock, which is
28
+ -- safe on a v1 SaaS members table (small) but should be benchmarked on a
29
+ -- large multi-tenant install before rolling.
30
+ --
31
+ -- See 0020_unified_user_token.sql header for why this file does NOT wrap in
32
+ -- BEGIN;/COMMIT; — Drizzle migrator already runs every pending migration
33
+ -- inside a single outer transaction.
34
+
35
+ -- ---------------------------------------------------------------------------
36
+ -- 1. auth_identities
37
+ -- ---------------------------------------------------------------------------
38
+ CREATE TABLE IF NOT EXISTS "auth_identities" (
39
+ "id" text PRIMARY KEY NOT NULL,
40
+ "user_id" text NOT NULL,
41
+ "provider" text NOT NULL,
42
+ "identifier" text NOT NULL,
43
+ "email" text,
44
+ "verified_at" timestamp with time zone,
45
+ "credential_type" text,
46
+ "credential_payload" jsonb,
47
+ "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
48
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
49
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
50
+ CONSTRAINT "uq_auth_identities_provider_identifier" UNIQUE ("provider", "identifier")
51
+ );
52
+
53
+ --> statement-breakpoint
54
+ ALTER TABLE "auth_identities"
55
+ ADD CONSTRAINT "auth_identities_user_id_users_id_fk"
56
+ FOREIGN KEY ("user_id") REFERENCES "users"("id")
57
+ ON DELETE cascade ON UPDATE no action;
58
+
59
+ --> statement-breakpoint
60
+ CREATE INDEX IF NOT EXISTS "idx_auth_identities_user" ON "auth_identities" ("user_id");
61
+ --> statement-breakpoint
62
+ CREATE INDEX IF NOT EXISTS "idx_auth_identities_email" ON "auth_identities" ("email");
63
+
64
+ -- ---------------------------------------------------------------------------
65
+ -- 2. invitations + invitation_redemptions
66
+ -- ---------------------------------------------------------------------------
67
+ --> statement-breakpoint
68
+ CREATE TABLE IF NOT EXISTS "invitations" (
69
+ "id" text PRIMARY KEY NOT NULL,
70
+ "organization_id" text NOT NULL,
71
+ "token" text NOT NULL UNIQUE,
72
+ "role" text DEFAULT 'member' NOT NULL,
73
+ "expires_at" timestamp with time zone,
74
+ "revoked_at" timestamp with time zone,
75
+ "created_by" text NOT NULL,
76
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
77
+ );
78
+
79
+ --> statement-breakpoint
80
+ ALTER TABLE "invitations"
81
+ ADD CONSTRAINT "invitations_organization_id_organizations_id_fk"
82
+ FOREIGN KEY ("organization_id") REFERENCES "organizations"("id")
83
+ ON DELETE cascade ON UPDATE no action;
84
+ --> statement-breakpoint
85
+ ALTER TABLE "invitations"
86
+ ADD CONSTRAINT "invitations_created_by_users_id_fk"
87
+ FOREIGN KEY ("created_by") REFERENCES "users"("id")
88
+ ON DELETE no action ON UPDATE no action;
89
+
90
+ --> statement-breakpoint
91
+ CREATE INDEX IF NOT EXISTS "idx_invitations_token" ON "invitations" ("token");
92
+ --> statement-breakpoint
93
+ CREATE INDEX IF NOT EXISTS "idx_invitations_org" ON "invitations" ("organization_id");
94
+
95
+ --> statement-breakpoint
96
+ -- v1 enforced rule: each org may have at most one non-revoked invitation.
97
+ -- The predicate is intentionally `revoked_at IS NULL` only — Postgres rejects
98
+ -- `now()` in an index predicate (must be IMMUTABLE), and conflating "expired"
99
+ -- with "no longer the active link" matches the v1 service contract anyway.
100
+ -- The runtime "is this still usable" filter (which DOES check `expires_at`)
101
+ -- lives in services/invitation.ts. Future "multiple links per org" relaxes by
102
+ -- dropping this index.
103
+ CREATE UNIQUE INDEX IF NOT EXISTS "uq_invitations_active_per_org"
104
+ ON "invitations" ("organization_id")
105
+ WHERE "revoked_at" IS NULL;
106
+
107
+ --> statement-breakpoint
108
+ CREATE TABLE IF NOT EXISTS "invitation_redemptions" (
109
+ "id" text PRIMARY KEY NOT NULL,
110
+ "invitation_id" text NOT NULL,
111
+ "user_id" text NOT NULL,
112
+ "redeemed_at" timestamp with time zone DEFAULT now() NOT NULL,
113
+ "ip" text,
114
+ "user_agent" text
115
+ );
116
+
117
+ --> statement-breakpoint
118
+ ALTER TABLE "invitation_redemptions"
119
+ ADD CONSTRAINT "invitation_redemptions_invitation_id_invitations_id_fk"
120
+ FOREIGN KEY ("invitation_id") REFERENCES "invitations"("id")
121
+ ON DELETE cascade ON UPDATE no action;
122
+ --> statement-breakpoint
123
+ ALTER TABLE "invitation_redemptions"
124
+ ADD CONSTRAINT "invitation_redemptions_user_id_users_id_fk"
125
+ FOREIGN KEY ("user_id") REFERENCES "users"("id")
126
+ ON DELETE cascade ON UPDATE no action;
127
+
128
+ --> statement-breakpoint
129
+ CREATE INDEX IF NOT EXISTS "idx_invitation_redemptions_invitation"
130
+ ON "invitation_redemptions" ("invitation_id");
131
+ --> statement-breakpoint
132
+ CREATE INDEX IF NOT EXISTS "idx_invitation_redemptions_user"
133
+ ON "invitation_redemptions" ("user_id");
134
+
135
+ -- ---------------------------------------------------------------------------
136
+ -- 3. members.status — soft-delete marker for "leave team"
137
+ -- ---------------------------------------------------------------------------
138
+ --
139
+ -- No partial index on `status='active'` is created in v1. Filter sites are:
140
+ -- - middleware/member-auth.ts: lookup by members.id (already PK-indexed)
141
+ -- - services/auth.ts (password login): WHERE user_id = ? AND status='active'
142
+ -- - services/membership.ts (listActiveMemberships): same filter
143
+ -- The existing `idx_members_user (user_id)` already collapses each user's
144
+ -- members rows to ~1-5 typical, so the in-page status check is essentially
145
+ -- free at the v1 SaaS scale (low six-digit users × low single-digit teams).
146
+ -- A partial unique-eligible index becomes worth adding when:
147
+ -- - members > ~100k rows AND
148
+ -- - average rows-per-user > ~50
149
+ -- whichever comes first. At that point the migration is a single
150
+ -- `CREATE INDEX CONCURRENTLY ... WHERE status='active'`.
151
+ --> statement-breakpoint
152
+ ALTER TABLE "members"
153
+ ADD COLUMN IF NOT EXISTS "status" text DEFAULT 'active' NOT NULL;
@@ -183,6 +183,13 @@
183
183
  "when": 1777420800000,
184
184
  "tag": "0025_inbox_silent_entries",
185
185
  "breakpoints": true
186
+ },
187
+ {
188
+ "idx": 26,
189
+ "version": "7",
190
+ "when": 1777507200000,
191
+ "tag": "0026_saas_onboarding",
192
+ "breakpoints": true
186
193
  }
187
194
  ]
188
195
  }
@@ -0,0 +1,52 @@
1
+ import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
2
+ import { r as AGENT_SELECTOR_HEADER } from "./dist-CLiN7cVS.mjs";
3
+ //#region src/core/feishu.ts
4
+ var feishu_exports = /* @__PURE__ */ __exportAll({
5
+ bindFeishuBot: () => bindFeishuBot,
6
+ bindFeishuUser: () => bindFeishuUser
7
+ });
8
+ /**
9
+ * Feishu-related core operations: bind-bot, bind-user.
10
+ *
11
+ * All agent-scoped calls carry both the member access JWT (Authorization)
12
+ * and the acting agent UUID (X-Agent-Id); the server's agent-selector
13
+ * middleware enforces Rule R-RUN.
14
+ */
15
+ async function bindFeishuBot(serverUrl, accessToken, agentId, appId, appSecret) {
16
+ const res = await fetch(`${serverUrl}/api/v1/agent/me/feishu-bot`, {
17
+ method: "PUT",
18
+ headers: {
19
+ Authorization: `Bearer ${accessToken}`,
20
+ [AGENT_SELECTOR_HEADER]: agentId,
21
+ "Content-Type": "application/json"
22
+ },
23
+ body: JSON.stringify({
24
+ appId,
25
+ appSecret
26
+ })
27
+ });
28
+ if (!res.ok) {
29
+ const body = await res.json().catch(() => ({}));
30
+ throw new Error(body.error ?? `Bind Feishu bot failed: HTTP ${res.status}`);
31
+ }
32
+ }
33
+ async function bindFeishuUser(serverUrl, accessToken, agentId, humanAgentId, feishuUserId, displayName) {
34
+ const res = await fetch(`${serverUrl}/api/v1/agent/delegated/${encodeURIComponent(humanAgentId)}/feishu-user`, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Bearer ${accessToken}`,
38
+ [AGENT_SELECTOR_HEADER]: agentId,
39
+ "Content-Type": "application/json"
40
+ },
41
+ body: JSON.stringify({
42
+ feishuUserId,
43
+ displayName
44
+ })
45
+ });
46
+ if (!res.ok) {
47
+ const body = await res.json().catch(() => ({}));
48
+ throw new Error(body.error ?? `Bind Feishu user failed: HTTP ${res.status}`);
49
+ }
50
+ }
51
+ //#endregion
52
+ export { bindFeishuUser as n, feishu_exports as r, bindFeishuBot as t };
@@ -1,5 +1,5 @@
1
1
  import { f as __require, l as __commonJSMin, n as init_esm, p as __toCommonJS, t as esm_exports } from "./esm-CYu4tXXn.mjs";
2
- import { t as require_execAsync } from "./execAsync-CP8iWV5b.mjs";
2
+ import { t as require_execAsync } from "./execAsync-XMc-nFn-.mjs";
3
3
  //#region ../../node_modules/.pnpm/@opentelemetry+resources@2.7.0_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/resources/build/src/detectors/platform/node/machine-id/getMachineId-bsd.js
4
4
  var require_getMachineId_bsd = /* @__PURE__ */ __commonJSMin(((exports) => {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,5 @@
1
1
  import { l as __commonJSMin, n as init_esm, p as __toCommonJS, t as esm_exports } from "./esm-CYu4tXXn.mjs";
2
- import { t as require_execAsync } from "./execAsync-CP8iWV5b.mjs";
2
+ import { t as require_execAsync } from "./execAsync-XMc-nFn-.mjs";
3
3
  //#region ../../node_modules/.pnpm/@opentelemetry+resources@2.7.0_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/resources/build/src/detectors/platform/node/machine-id/getMachineId-darwin.js
4
4
  var require_getMachineId_darwin = /* @__PURE__ */ __commonJSMin(((exports) => {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,5 @@
1
1
  import { f as __require, l as __commonJSMin, n as init_esm, p as __toCommonJS, t as esm_exports } from "./esm-CYu4tXXn.mjs";
2
- import { t as require_execAsync } from "./execAsync-CP8iWV5b.mjs";
2
+ import { t as require_execAsync } from "./execAsync-XMc-nFn-.mjs";
3
3
  //#region ../../node_modules/.pnpm/@opentelemetry+resources@2.7.0_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/resources/build/src/detectors/platform/node/machine-id/getMachineId-win.js
4
4
  var require_getMachineId_win = /* @__PURE__ */ __commonJSMin(((exports) => {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
- import "./observability-DV_fQKqV-oxfXX6Z2.mjs";
2
- import { A as installClientService, B as createOwner, C as checkNodeVersion, D as checkWebSocket, E as checkServerReachable, F as isDockerAvailable, I as stopPostgres, J as FirstTreeHubSDK, K as status, L as ClientRuntime, M as resolveCliInvocation, N as uninstallClientService, O as printResults, P as ensurePostgres, R as handleClientOrgMismatch, S as checkDocker, T as checkServerHealth, U as blank, V as hasUser, Y as SdkError, _ as runMigrations, b as checkClientConfig, c as promptMissingFields, d as onboardCheck, f as onboardCreate, i as startServer, j as isServiceSupported, k as getClientServiceStatus, l as formatCheckReport, m as runHomeMigration, o as isInteractive, s as promptAddAgent, v as checkAgentConfigs, w as checkServerConfig, x as checkDatabase, z as rotateClientIdWithBackup } from "./core-BgiFGT7Y.mjs";
1
+ import "./observability-DPyf745N-BSc8QNcR.mjs";
2
+ import { $ as FirstTreeHubSDK, A as checkWebSocket, B as ClientRuntime, C as checkClientConfig, D as checkServerConfig, E as checkNodeVersion, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as uninstallClientService, J as blank, L as ensurePostgres, M as getClientServiceStatus, N as installClientService, O as checkServerHealth, P as isServiceSupported, R as isDockerAvailable, T as checkDocker, U as createOwner, V as handleClientOrgMismatch, W as hasUser, Z as status, _ as runHomeMigration, b as runMigrations, d as promptMissingFields, et as SdkError, f as formatCheckReport, h as onboardCreate, j as printResults, k as checkServerReachable, l as isInteractive, m as onboardCheck, n as deriveHubUrlFromToken, s as startServer, t as HubUrlDerivationError, u as promptAddAgent, w as checkDatabase, x as checkAgentConfigs, z as stopPostgres } from "./saas-connect-idjpoPTk.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CtVqQA8a.mjs";
5
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DEmwoNn_.mjs";
6
- export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, rotateClientIdWithBackup, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };
4
+ import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-Ca5Fiqz6.mjs";
5
+ import "./dist-CLiN7cVS.mjs";
6
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-FTWnoOsc.mjs";
7
+ import "./invitation-C_zAhB8x-8Khychlu.mjs";
8
+ export { ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, rotateClientIdWithBackup, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };
@@ -0,0 +1,3 @@
1
+ import "./dist-CLiN7cVS.mjs";
2
+ import { _ as rotateInvitation, h as previewInvitation } from "./invitation-C_zAhB8x-8Khychlu.mjs";
3
+ export { previewInvitation, rotateInvitation };