@agent-team-foundation/first-tree-hub 0.10.1 → 0.10.3
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/dist/{bootstrap-CtVqQA8a.mjs → bootstrap-CBAVWQUT.mjs} +9 -1
- package/dist/cli/index.mjs +63 -20
- package/dist/{feishu-DEmwoNn_.mjs → dist-DUCelK3Z.mjs} +202 -62
- package/dist/drizzle/0026_saas_onboarding.sql +153 -0
- package/dist/drizzle/0027_runtime_provider.sql +10 -0
- package/dist/drizzle/0028_auth_identity_user_github_unique.sql +12 -0
- package/dist/drizzle/meta/_journal.json +21 -0
- package/dist/feishu-Boy3n8CT.mjs +52 -0
- package/dist/{getMachineId-bsd-BB-fnFLA.mjs → getMachineId-bsd-D0w3uAZa.mjs} +1 -1
- package/dist/{getMachineId-darwin-DAYWNsYK.mjs → getMachineId-darwin-DOoYFb2_.mjs} +1 -1
- package/dist/{getMachineId-win-H5RT49ov.mjs → getMachineId-win-B6hY8edq.mjs} +1 -1
- package/dist/index.mjs +7 -5
- package/dist/invitation-BTlGMy0o-Coj07kYi.mjs +3 -0
- package/dist/invitation-C_zAhB8x-8Khychlu.mjs +258 -0
- package/dist/{observability-DDkJwSKv.mjs → observability-C08jUFsJ.mjs} +1 -1
- package/dist/{observability-DV_fQKqV-oxfXX6Z2.mjs → observability-DPyf745N-BSc8QNcR.mjs} +6 -6
- package/dist/{core-BgiFGT7Y.mjs → saas-connect-3p-vBkuY.mjs} +2459 -430
- package/dist/web/assets/index-CHoaSIzI.js +21 -0
- package/dist/web/assets/index-CP8uLPyO.css +1 -0
- package/dist/web/assets/index-D7OzKrI2.js +387 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/dist/web/assets/index-Cd290Lq6.css +0 -1
- package/dist/web/assets/index-xi7JmCtW.js +0 -361
- /package/dist/{execAsync-CP8iWV5b.mjs → execAsync-XMc-nFn-.mjs} +0 -0
- /package/dist/{getMachineId-linux-BU7Fi6S0.mjs → getMachineId-linux-MlY63Zsw.mjs} +0 -0
- /package/dist/{getMachineId-unsupported-BhWCxKBo.mjs → getMachineId-unsupported-BS652RIy.mjs} +0 -0
|
@@ -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;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Add runtime_provider to agents.
|
|
2
|
+
--
|
|
3
|
+
-- Tags each agent with the runtime that drives it (e.g. "claude-code", "codex").
|
|
4
|
+
-- DEFAULT 'claude-code' backfills every existing row so the NOT NULL constraint
|
|
5
|
+
-- is safe to land in a single step. Hub deploys are stop-migrate-restart, not
|
|
6
|
+
-- rolling, so we don't need a two-phase add (nullable → backfill → not null).
|
|
7
|
+
--
|
|
8
|
+
-- Capabilities reporting reuses the existing `clients.metadata` jsonb column
|
|
9
|
+
-- under the `capabilities` subkey (Option C); no SQL change for clients.
|
|
10
|
+
ALTER TABLE "agents" ADD COLUMN "runtime_provider" text DEFAULT 'claude-code' NOT NULL;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Partial unique index: each user can hold at most one github identity.
|
|
2
|
+
--
|
|
3
|
+
-- Defense-in-depth. The (provider, identifier) UNIQUE catches duplicates
|
|
4
|
+
-- of the SAME githubId, but does not stop a single user from collecting
|
|
5
|
+
-- multiple DIFFERENT githubIds (e.g. a future "merge accounts" or
|
|
6
|
+
-- "rebind" flow that misfires, or a one-off SQL migration that errs).
|
|
7
|
+
-- This index makes any such double-bind fail atomically with a
|
|
8
|
+
-- unique-violation at the storage layer.
|
|
9
|
+
|
|
10
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "uq_auth_identities_user_github"
|
|
11
|
+
ON "auth_identities" ("user_id")
|
|
12
|
+
WHERE "provider" = 'github';
|
|
@@ -183,6 +183,27 @@
|
|
|
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
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"idx": 27,
|
|
196
|
+
"version": "7",
|
|
197
|
+
"when": 1777593600000,
|
|
198
|
+
"tag": "0027_runtime_provider",
|
|
199
|
+
"breakpoints": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"idx": 28,
|
|
203
|
+
"version": "7",
|
|
204
|
+
"when": 1777680000000,
|
|
205
|
+
"tag": "0028_auth_identity_user_github_unique",
|
|
206
|
+
"breakpoints": true
|
|
186
207
|
}
|
|
187
208
|
]
|
|
188
209
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
|
|
2
|
+
import { r as AGENT_SELECTOR_HEADER } from "./dist-DUCelK3Z.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-
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { A as
|
|
1
|
+
import "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
2
|
+
import { $ as status, A as checkServerHealth, B as isDockerAvailable, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as installClientService, G as createOwner, H as ClientRuntime, I as isServiceSupported, K as hasUser, L as resolveCliInvocation, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as getClientServiceStatus, R as uninstallClientService, S as runMigrations, T as checkClientConfig, U as handleClientOrgMismatch, V as stopPostgres, W as rotateClientIdWithBackup, X as blank, _ as onboardCreate, d as isInteractive, f as promptAddAgent, g as onboardCheck, j as checkServerReachable, k as checkServerConfig, m as formatCheckReport, n as deriveHubUrlFromToken, nt as SdkError, p as promptMissingFields, s as startServer, t as HubUrlDerivationError, tt as FirstTreeHubSDK, y as runHomeMigration, z as ensurePostgres } from "./saas-connect-3p-vBkuY.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-
|
|
5
|
-
import
|
|
6
|
-
|
|
4
|
+
import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CBAVWQUT.mjs";
|
|
5
|
+
import "./dist-DUCelK3Z.mjs";
|
|
6
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-Boy3n8CT.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,258 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { and, desc, eq, gt, isNull, or } from "drizzle-orm";
|
|
3
|
+
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
//#region ../server/dist/invitation-C_zAhB8x.mjs
|
|
5
|
+
/** Organization entity. Agents and chats belong to exactly one organization. */
|
|
6
|
+
const organizations = pgTable("organizations", {
|
|
7
|
+
id: text("id").primaryKey(),
|
|
8
|
+
name: text("name").unique().notNull(),
|
|
9
|
+
displayName: text("display_name").notNull(),
|
|
10
|
+
maxAgents: integer("max_agents").notNull().default(0),
|
|
11
|
+
maxMessagesPerMinute: integer("max_messages_per_minute").notNull().default(0),
|
|
12
|
+
features: jsonb("features").$type().notNull().default({}),
|
|
13
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
14
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
15
|
+
});
|
|
16
|
+
/** User accounts. Passwords are stored as bcrypt hashes. */
|
|
17
|
+
const users = pgTable("users", {
|
|
18
|
+
id: text("id").primaryKey(),
|
|
19
|
+
username: text("username").unique().notNull(),
|
|
20
|
+
passwordHash: text("password_hash").notNull(),
|
|
21
|
+
displayName: text("display_name").notNull(),
|
|
22
|
+
avatarUrl: text("avatar_url"),
|
|
23
|
+
status: text("status").notNull().default("active"),
|
|
24
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
25
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
26
|
+
});
|
|
27
|
+
var AppError = class extends Error {
|
|
28
|
+
constructor(statusCode, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.statusCode = statusCode;
|
|
31
|
+
this.name = "AppError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var NotFoundError = class extends AppError {
|
|
35
|
+
constructor(message = "Not found") {
|
|
36
|
+
super(404, message);
|
|
37
|
+
this.name = "NotFoundError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var UnauthorizedError = class extends AppError {
|
|
41
|
+
constructor(message = "Unauthorized") {
|
|
42
|
+
super(401, message);
|
|
43
|
+
this.name = "UnauthorizedError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var ForbiddenError = class extends AppError {
|
|
47
|
+
constructor(message = "Forbidden") {
|
|
48
|
+
super(403, message);
|
|
49
|
+
this.name = "ForbiddenError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ConflictError = class extends AppError {
|
|
53
|
+
constructor(message = "Conflict") {
|
|
54
|
+
super(409, message);
|
|
55
|
+
this.name = "ConflictError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var BadRequestError = class extends AppError {
|
|
59
|
+
constructor(message = "Bad request") {
|
|
60
|
+
super(400, message);
|
|
61
|
+
this.name = "BadRequestError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when an operation targets a client whose organization does not match
|
|
66
|
+
* the caller's authenticated organization. A client is bound to exactly one
|
|
67
|
+
* org for its lifetime; re-registering or operating under a different org's
|
|
68
|
+
* credentials is refused. CLI consumers recognize the `code` field and
|
|
69
|
+
* respond by abandoning the local clientId to register a fresh one.
|
|
70
|
+
*/
|
|
71
|
+
var ClientOrgMismatchError = class extends AppError {
|
|
72
|
+
code = "CLIENT_ORG_MISMATCH";
|
|
73
|
+
constructor(message = "Client belongs to a different organization") {
|
|
74
|
+
super(403, message);
|
|
75
|
+
this.name = "ClientOrgMismatchError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/** Generate a UUID v7 (time-ordered). No external dependency. */
|
|
79
|
+
function uuidv7() {
|
|
80
|
+
const now = BigInt(Date.now());
|
|
81
|
+
const bytes = new Uint8Array(16);
|
|
82
|
+
bytes[0] = Number(now >> 40n & 255n);
|
|
83
|
+
bytes[1] = Number(now >> 32n & 255n);
|
|
84
|
+
bytes[2] = Number(now >> 24n & 255n);
|
|
85
|
+
bytes[3] = Number(now >> 16n & 255n);
|
|
86
|
+
bytes[4] = Number(now >> 8n & 255n);
|
|
87
|
+
bytes[5] = Number(now & 255n);
|
|
88
|
+
const rand = randomBytes(10);
|
|
89
|
+
for (let i = 0; i < 10; i++) {
|
|
90
|
+
const b = rand[i];
|
|
91
|
+
if (b !== void 0) bytes[6 + i] = b;
|
|
92
|
+
}
|
|
93
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 112;
|
|
94
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
95
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
96
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Org-level invitation links. v1 enforces "one active link per org" via a
|
|
100
|
+
* partial UNIQUE index added in the SQL migration (Drizzle's TS DSL does not
|
|
101
|
+
* yet model partial uniques). Rotating a link sets `revoked_at` on the prior
|
|
102
|
+
* row and inserts a new one in the same transaction; revoked rows stay for
|
|
103
|
+
* audit but no longer satisfy the partial uniqueness predicate.
|
|
104
|
+
*
|
|
105
|
+
* `role` is fixed to `'member'` by the v1 API but stored on the row so a
|
|
106
|
+
* future "invite as admin" feature is a route change, not a schema change.
|
|
107
|
+
*
|
|
108
|
+
* `expires_at` is left unset by the v1 rotate flow — invite links don't
|
|
109
|
+
* auto-expire. The column exists so an admin can opt into expiry later (and
|
|
110
|
+
* the partial unique predicate already filters on it).
|
|
111
|
+
*/
|
|
112
|
+
const invitations = pgTable("invitations", {
|
|
113
|
+
id: text("id").primaryKey(),
|
|
114
|
+
organizationId: text("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
|
115
|
+
token: text("token").notNull().unique(),
|
|
116
|
+
role: text("role").notNull().default("member"),
|
|
117
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
118
|
+
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
119
|
+
createdBy: text("created_by").notNull().references(() => users.id),
|
|
120
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
121
|
+
}, (table) => [index("idx_invitations_token").on(table.token), index("idx_invitations_org").on(table.organizationId)]);
|
|
122
|
+
/** Audit row for every successful redemption — v1 admins inspect via DB. */
|
|
123
|
+
const invitationRedemptions = pgTable("invitation_redemptions", {
|
|
124
|
+
id: text("id").primaryKey(),
|
|
125
|
+
invitationId: text("invitation_id").notNull().references(() => invitations.id, { onDelete: "cascade" }),
|
|
126
|
+
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
127
|
+
redeemedAt: timestamp("redeemed_at", { withTimezone: true }).notNull().defaultNow(),
|
|
128
|
+
ip: text("ip"),
|
|
129
|
+
userAgent: text("user_agent")
|
|
130
|
+
}, (table) => [index("idx_invitation_redemptions_invitation").on(table.invitationId), index("idx_invitation_redemptions_user").on(table.userId)]);
|
|
131
|
+
const TOKEN_BYTES = 32;
|
|
132
|
+
/**
|
|
133
|
+
* Default invite-link TTL — authoritative server-side value. Tightening
|
|
134
|
+
* "anyone with this link can join" to a bounded window is the primary
|
|
135
|
+
* mitigation for accidental link leakage (admin pasting into a public
|
|
136
|
+
* Slack channel, forwarded email chains, screen-share captures, etc).
|
|
137
|
+
* 7 days mirrors what GitHub and Vercel default to; longer windows put
|
|
138
|
+
* more leak surface on the same token. Admins extend by clicking Rotate
|
|
139
|
+
* (which mints a fresh 7-day link in one transaction).
|
|
140
|
+
*
|
|
141
|
+
* The mirror constant in `@…shared/schemas/invitation.ts` exists so the
|
|
142
|
+
* web UI can render "expires in 7 days" copy without an extra round-trip.
|
|
143
|
+
*/
|
|
144
|
+
const INVITATION_DEFAULT_TTL_MS = 10080 * 60 * 1e3;
|
|
145
|
+
function generateInvitationToken() {
|
|
146
|
+
return randomBytes(TOKEN_BYTES).toString("base64url");
|
|
147
|
+
}
|
|
148
|
+
function defaultExpiry() {
|
|
149
|
+
return new Date(Date.now() + INVITATION_DEFAULT_TTL_MS);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Return the *active* invitation for `orgId`, or null if none exists.
|
|
153
|
+
* "Active" = not revoked AND not expired.
|
|
154
|
+
*
|
|
155
|
+
* Mirrors the predicate of the partial UNIQUE index `uq_invitations_active_per_org`,
|
|
156
|
+
* so a successful `getActiveInvitation` is the same row the index protects.
|
|
157
|
+
*/
|
|
158
|
+
async function getActiveInvitation(db, orgId) {
|
|
159
|
+
const now = /* @__PURE__ */ new Date();
|
|
160
|
+
const [row] = await db.select().from(invitations).where(and(eq(invitations.organizationId, orgId), isNull(invitations.revokedAt), or(isNull(invitations.expiresAt), gt(invitations.expiresAt, now)))).orderBy(desc(invitations.createdAt)).limit(1);
|
|
161
|
+
return row ?? null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get-or-create the active invitation for `orgId`. Idempotent so the admin
|
|
165
|
+
* UI can call this on first render without inadvertently creating a fresh
|
|
166
|
+
* link every time someone visits Settings.
|
|
167
|
+
*
|
|
168
|
+
* "Active" is filtered by `getActiveInvitation` (revoked_at IS NULL AND
|
|
169
|
+
* not expired). When no active row exists we delegate to `rotateInvitation`
|
|
170
|
+
* — that path correctly handles the case where a prior row exists but
|
|
171
|
+
* has expired (`revoked_at IS NULL` but `expires_at < now()`). A naked
|
|
172
|
+
* INSERT here would trip `uq_invitations_active_per_org` (the partial
|
|
173
|
+
* unique index can't filter on `now()`, so it considers expired-but-not-
|
|
174
|
+
* revoked rows as still occupying the slot).
|
|
175
|
+
*/
|
|
176
|
+
async function ensureActiveInvitation(db, orgId, createdBy) {
|
|
177
|
+
const existing = await getActiveInvitation(db, orgId);
|
|
178
|
+
if (existing) return existing;
|
|
179
|
+
return rotateInvitation(db, orgId, createdBy);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Rotate the invitation: revoke every non-revoked row for this org (the
|
|
183
|
+
* current active link AND any expired-but-not-revoked stragglers) and
|
|
184
|
+
* insert a fresh one in a single transaction. Old tokens stop redeeming
|
|
185
|
+
* immediately. The new row carries a default 7-day expiry; admin extends
|
|
186
|
+
* by rotating again.
|
|
187
|
+
*/
|
|
188
|
+
async function rotateInvitation(db, orgId, createdBy) {
|
|
189
|
+
return db.transaction(async (tx) => {
|
|
190
|
+
const now = /* @__PURE__ */ new Date();
|
|
191
|
+
await tx.update(invitations).set({ revokedAt: now }).where(and(eq(invitations.organizationId, orgId), isNull(invitations.revokedAt)));
|
|
192
|
+
const id = uuidv7();
|
|
193
|
+
const token = generateInvitationToken();
|
|
194
|
+
const [row] = await tx.insert(invitations).values({
|
|
195
|
+
id,
|
|
196
|
+
organizationId: orgId,
|
|
197
|
+
token,
|
|
198
|
+
role: "member",
|
|
199
|
+
createdBy,
|
|
200
|
+
expiresAt: defaultExpiry()
|
|
201
|
+
}).returning();
|
|
202
|
+
if (!row) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
203
|
+
return row;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Look up an invitation by its public token. Returns null when the token
|
|
208
|
+
* is unknown OR when the row exists but is no longer active. Conflating
|
|
209
|
+
* "unknown" with "revoked" prevents an attacker from inferring which
|
|
210
|
+
* tokens were once valid.
|
|
211
|
+
*/
|
|
212
|
+
async function findActiveByToken(db, token) {
|
|
213
|
+
const now = /* @__PURE__ */ new Date();
|
|
214
|
+
const [row] = await db.select().from(invitations).where(and(eq(invitations.token, token), isNull(invitations.revokedAt), or(isNull(invitations.expiresAt), gt(invitations.expiresAt, now)))).limit(1);
|
|
215
|
+
return row ?? null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Public preview surfaced on `/invite/:token` before the recipient signs in.
|
|
219
|
+
*/
|
|
220
|
+
async function previewInvitation(db, token) {
|
|
221
|
+
const inv = await findActiveByToken(db, token);
|
|
222
|
+
if (!inv) throw new NotFoundError("Invitation not found or no longer valid");
|
|
223
|
+
const [org] = await db.select({
|
|
224
|
+
id: organizations.id,
|
|
225
|
+
name: organizations.name,
|
|
226
|
+
displayName: organizations.displayName
|
|
227
|
+
}).from(organizations).where(eq(organizations.id, inv.organizationId)).limit(1);
|
|
228
|
+
if (!org) throw new NotFoundError("Invitation organization not found");
|
|
229
|
+
return {
|
|
230
|
+
organizationId: org.id,
|
|
231
|
+
organizationName: org.name,
|
|
232
|
+
organizationDisplayName: org.displayName,
|
|
233
|
+
role: inv.role
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Record a redemption row. Caller is responsible for executing the join
|
|
238
|
+
* (members.insert / status='active' flip) — this is the audit trail only.
|
|
239
|
+
*/
|
|
240
|
+
async function recordRedemption(db, data) {
|
|
241
|
+
await db.insert(invitationRedemptions).values({
|
|
242
|
+
id: uuidv7(),
|
|
243
|
+
invitationId: data.invitationId,
|
|
244
|
+
userId: data.userId,
|
|
245
|
+
ip: data.ip ?? null,
|
|
246
|
+
userAgent: data.userAgent ?? null
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build the invite URL surfaced to admins. `publicUrl` should be the
|
|
251
|
+
* server's `server.publicUrl` config; pass the request host as fallback in
|
|
252
|
+
* dev where publicUrl may be unset.
|
|
253
|
+
*/
|
|
254
|
+
function buildInviteUrl(publicUrl, token) {
|
|
255
|
+
return `${publicUrl.replace(/\/+$/, "")}/invite/${token}`;
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
export { rotateInvitation as _, ForbiddenError as a, buildInviteUrl as c, getActiveInvitation as d, invitationRedemptions as f, recordRedemption as g, previewInvitation as h, ConflictError as i, ensureActiveInvitation as l, organizations as m, BadRequestError as n, NotFoundError as o, invitations as p, ClientOrgMismatchError as r, UnauthorizedError as s, AppError as t, findActiveByToken as u, users as v, uuidv7 as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./esm-CYu4tXXn.mjs";
|
|
2
|
-
import { m as shutdownTelemetry, s as initTelemetry } from "./observability-
|
|
2
|
+
import { m as shutdownTelemetry, s as initTelemetry } from "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
3
|
import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
4
4
|
export { initTelemetry, shutdownTelemetry };
|
|
@@ -28384,19 +28384,19 @@ var require_getMachineId = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
28384
28384
|
async function getMachineId() {
|
|
28385
28385
|
if (!getMachineIdImpl) switch (process$1.platform) {
|
|
28386
28386
|
case "darwin":
|
|
28387
|
-
getMachineIdImpl = (await import("./getMachineId-darwin-
|
|
28387
|
+
getMachineIdImpl = (await import("./getMachineId-darwin-DOoYFb2_.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
28388
28388
|
break;
|
|
28389
28389
|
case "linux":
|
|
28390
|
-
getMachineIdImpl = (await import("./getMachineId-linux-
|
|
28390
|
+
getMachineIdImpl = (await import("./getMachineId-linux-MlY63Zsw.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
28391
28391
|
break;
|
|
28392
28392
|
case "freebsd":
|
|
28393
|
-
getMachineIdImpl = (await import("./getMachineId-bsd-
|
|
28393
|
+
getMachineIdImpl = (await import("./getMachineId-bsd-D0w3uAZa.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
28394
28394
|
break;
|
|
28395
28395
|
case "win32":
|
|
28396
|
-
getMachineIdImpl = (await import("./getMachineId-win-
|
|
28396
|
+
getMachineIdImpl = (await import("./getMachineId-win-B6hY8edq.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
28397
28397
|
break;
|
|
28398
28398
|
default:
|
|
28399
|
-
getMachineIdImpl = (await import("./getMachineId-unsupported-
|
|
28399
|
+
getMachineIdImpl = (await import("./getMachineId-unsupported-BS652RIy.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
28400
28400
|
break;
|
|
28401
28401
|
}
|
|
28402
28402
|
return getMachineIdImpl();
|
|
@@ -33467,7 +33467,7 @@ var require_src = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
33467
33467
|
});
|
|
33468
33468
|
}));
|
|
33469
33469
|
//#endregion
|
|
33470
|
-
//#region ../server/dist/observability-
|
|
33470
|
+
//#region ../server/dist/observability-DPyf745N.mjs
|
|
33471
33471
|
var import_pino = /* @__PURE__ */ __toESM(require_pino(), 1);
|
|
33472
33472
|
var import_otel = /* @__PURE__ */ __toESM(require_otel(), 1);
|
|
33473
33473
|
init_esm$2();
|