@agent-team-foundation/first-tree-hub 0.10.1 → 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-Cd290Lq6.css +0 -1
  21. package/dist/web/assets/index-xi7JmCtW.js +0 -361
  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
@@ -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-DV_fQKqV-oxfXX6Z2.mjs";
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-DAYWNsYK.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-BU7Fi6S0.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-BB-fnFLA.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-H5RT49ov.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-BhWCxKBo.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-DV_fQKqV.mjs
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();