@agent-team-foundation/first-tree-hub 0.10.13 → 0.10.14
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-CDeXqhkQ.mjs → bootstrap-B2x4TTyJ.mjs} +33 -3
- package/dist/cli/index.mjs +43 -6
- package/dist/{dist-DwbhZyGi.mjs → dist-D6AOiyNg.mjs} +15 -0
- package/dist/{feishu-viiZmwcn.mjs → feishu-DQ1l18Ah.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/{invitation-CBnQyB7o-DLQyW5ek.mjs → invitation-CBnQyB7o-Bulf3Sl7.mjs} +1 -1
- package/dist/{saas-connect-DLVGb8OH.mjs → saas-connect-DWcxHtjX.mjs} +296 -159
- package/dist/web/assets/{index-dk86IMMq.js → index-BQda2sqe.js} +1 -1
- package/dist/web/assets/{index-BGMkYsML.js → index-C7yW7sWI.js} +85 -80
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { m as __toESM } from "./esm-CYu4tXXn.mjs";
|
|
2
2
|
import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
|
-
import { C as
|
|
4
|
-
import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-
|
|
3
|
+
import { C as resolveConfigReadonly, S as resetConfigMeta, T as setConfigValue, _ as initConfig, a as loadCredentials, c as saveAgentConfig, d as DEFAULT_DATA_DIR$1, f as DEFAULT_HOME_DIR$1, h as collectMissingPrompts, l as saveCredentials, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as serverConfigSchema, x as resetConfig, y as migrateLegacyHome } from "./bootstrap-B2x4TTyJ.mjs";
|
|
4
|
+
import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-D6AOiyNg.mjs";
|
|
5
5
|
import { _ as recordRedemption, a as ConflictError, b as uuidv7, c as UnauthorizedError, d as findActiveByToken, f as getActiveInvitation, h as organizations, i as ClientUserMismatchError$1, l as buildInviteUrl, m as invitations, n as BadRequestError, o as ForbiddenError, p as invitationRedemptions, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as ensureActiveInvitation, y as users } from "./invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { ZodError, z } from "zod";
|
|
@@ -742,10 +742,25 @@ z.object({
|
|
|
742
742
|
});
|
|
743
743
|
z.object({ agentId: z.string().min(1) });
|
|
744
744
|
const clientStatusSchema = z.enum(["connected", "disconnected"]);
|
|
745
|
+
/**
|
|
746
|
+
* Auth health channel surfaced to the Web admin dashboard. Computed
|
|
747
|
+
* server-side per request from the row's offline duration vs the
|
|
748
|
+
* configured refresh-token TTL — there is no DB column. See
|
|
749
|
+
* `deriveAuthState` server-side.
|
|
750
|
+
*
|
|
751
|
+
* - `ok` — online, or recently offline (cached refresh token can
|
|
752
|
+
* plausibly still mint access tokens).
|
|
753
|
+
* - `expired` — offline longer than the refresh-token TTL; the client
|
|
754
|
+
* cannot recover on its own. The operator mints a fresh
|
|
755
|
+
* connect token via the Web "+ New Connection" button
|
|
756
|
+
* (or the inline Reconnect button on the row).
|
|
757
|
+
*/
|
|
758
|
+
const clientAuthStateSchema = z.enum(["ok", "expired"]);
|
|
745
759
|
z.object({
|
|
746
760
|
id: z.string(),
|
|
747
761
|
userId: z.string().nullable(),
|
|
748
762
|
status: clientStatusSchema,
|
|
763
|
+
authState: clientAuthStateSchema,
|
|
749
764
|
sdkVersion: z.string().max(50).nullable(),
|
|
750
765
|
hostname: z.string().max(100).nullable(),
|
|
751
766
|
os: z.string().max(50).nullable(),
|
|
@@ -1517,6 +1532,11 @@ defineConfig({
|
|
|
1517
1532
|
secret: true
|
|
1518
1533
|
})
|
|
1519
1534
|
},
|
|
1535
|
+
auth: {
|
|
1536
|
+
accessTokenExpiry: field(z.string().default("30m"), { env: "FIRST_TREE_HUB_AUTH_ACCESS_TOKEN_EXPIRY" }),
|
|
1537
|
+
refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
|
|
1538
|
+
connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
|
|
1539
|
+
},
|
|
1520
1540
|
contextTree: optional({
|
|
1521
1541
|
repo: field(z.string(), {
|
|
1522
1542
|
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
@@ -2061,7 +2081,6 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2061
2081
|
}, WS_CONNECT_TIMEOUT_MS);
|
|
2062
2082
|
ws.on("open", async () => {
|
|
2063
2083
|
this.ws = ws;
|
|
2064
|
-
this.reconnectAttempt = 0;
|
|
2065
2084
|
this.wsLogger.debug("socket opened, sending auth");
|
|
2066
2085
|
try {
|
|
2067
2086
|
const token = await this.getAccessToken({ minValidityMs: AUTH_REFRESH_LEAD_MS + 5e3 });
|
|
@@ -2072,7 +2091,12 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2072
2091
|
this.scheduleProactiveAuthRefresh(token);
|
|
2073
2092
|
} catch (err) {
|
|
2074
2093
|
this.authLogger.error({ err }, "failed to obtain access token");
|
|
2075
|
-
|
|
2094
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2095
|
+
if (e.name === "AuthRefreshFailedError") {
|
|
2096
|
+
this.closing = true;
|
|
2097
|
+
this.emit("auth:fatal", e);
|
|
2098
|
+
}
|
|
2099
|
+
settle(reject, e);
|
|
2076
2100
|
ws.close();
|
|
2077
2101
|
}
|
|
2078
2102
|
});
|
|
@@ -2168,6 +2192,7 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2168
2192
|
if (type === "client:registered") {
|
|
2169
2193
|
const isReconnect = this.boundAgents.size > 0 || this.desiredBindings.size > 0;
|
|
2170
2194
|
this.registered = true;
|
|
2195
|
+
this.reconnectAttempt = 0;
|
|
2171
2196
|
this.startHeartbeat();
|
|
2172
2197
|
this.wsLogger.info({ isReconnect }, "registered");
|
|
2173
2198
|
this.emit("connected");
|
|
@@ -2320,6 +2345,7 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2320
2345
|
});
|
|
2321
2346
|
}
|
|
2322
2347
|
scheduleReconnect() {
|
|
2348
|
+
if (this.closing) return;
|
|
2323
2349
|
this.reconnectAttempt++;
|
|
2324
2350
|
this.emit("reconnecting", this.reconnectAttempt);
|
|
2325
2351
|
const delay = Math.min(RECONNECT_BASE_MS * 2 ** (this.reconnectAttempt - 1), RECONNECT_MAX_MS);
|
|
@@ -6267,6 +6293,14 @@ var ClientRuntime = class {
|
|
|
6267
6293
|
this.connection.on("auth:expired", () => {
|
|
6268
6294
|
print.status("⚠️", "access token expired — reconnecting after refresh...");
|
|
6269
6295
|
});
|
|
6296
|
+
this.connection.on("auth:fatal", (err) => {
|
|
6297
|
+
print.blank();
|
|
6298
|
+
print.status("✗", "auth expired — service is shutting down to break the reconnect loop.");
|
|
6299
|
+
print.status("", err.message);
|
|
6300
|
+
print.status("", "Recovery: get a new connect token from your Hub's Web admin");
|
|
6301
|
+
print.status("", " (Computers → + New Connection), then re-run the command shown.");
|
|
6302
|
+
process.exit(75);
|
|
6303
|
+
});
|
|
6270
6304
|
this.connection.on("error", (err) => {
|
|
6271
6305
|
print.status("⚠️", `client connection error: ${err.message}`);
|
|
6272
6306
|
});
|
|
@@ -8064,7 +8098,7 @@ async function onboardCreate(args) {
|
|
|
8064
8098
|
}
|
|
8065
8099
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
8066
8100
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
8067
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
8101
|
+
const { bindFeishuBot } = await import("./feishu-DQ1l18Ah.mjs").then((n) => n.r);
|
|
8068
8102
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
8069
8103
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
8070
8104
|
else {
|
|
@@ -9044,7 +9078,7 @@ function createFeedbackHandler(config) {
|
|
|
9044
9078
|
return { handle };
|
|
9045
9079
|
}
|
|
9046
9080
|
//#endregion
|
|
9047
|
-
//#region ../server/dist/app-
|
|
9081
|
+
//#region ../server/dist/app-Le92-WQA.mjs
|
|
9048
9082
|
var __defProp = Object.defineProperty;
|
|
9049
9083
|
var __exportAll = (all, no_symbols) => {
|
|
9050
9084
|
let target = {};
|
|
@@ -9183,7 +9217,32 @@ function requireMember(request) {
|
|
|
9183
9217
|
* - Manageability distinguishes roles: admin can manage all, member only their own.
|
|
9184
9218
|
* - All conditions include organizationId scoping to prevent cross-org access.
|
|
9185
9219
|
*/
|
|
9186
|
-
/**
|
|
9220
|
+
/**
|
|
9221
|
+
* Extract MemberScope from an authenticated request. Single definition, used by all routes.
|
|
9222
|
+
*
|
|
9223
|
+
* **Org-scoped admin routes must NOT use the result of this directly for
|
|
9224
|
+
* data filtering.** The returned scope is keyed to the JWT default member,
|
|
9225
|
+
* which is whatever org `auth.login` happened to pick at issuance time —
|
|
9226
|
+
* not the org the user has selected in the dropdown (`localStorage.
|
|
9227
|
+
* selectedOrganizationId`). Pair every `memberScope(request)` in
|
|
9228
|
+
* `packages/server/src/api/admin/*.ts` with one of:
|
|
9229
|
+
*
|
|
9230
|
+
* - `resolveAdminScope(db, request, baseScope, request.query.organizationId)`
|
|
9231
|
+
* for routes that accept `?organizationId=` and want a uniformly rotated
|
|
9232
|
+
* scope (every field keyed to the target org).
|
|
9233
|
+
* - `requireMemberInOrg(db, request, orgId)` when only the membership
|
|
9234
|
+
* `(memberId, role, agentId)` is needed for a specific org (e.g. the
|
|
9235
|
+
* creator HUMAN in chat-create).
|
|
9236
|
+
* - `assertCanManage(db, scope, agentUuid)` / `assertAgentVisible(db, scope, agentUuid)`
|
|
9237
|
+
* for routes operating on a single agent — both already rebind authority
|
|
9238
|
+
* to the agent's own org.
|
|
9239
|
+
*
|
|
9240
|
+
* `__tests__/admin-routes-org-scope-invariant.test.ts` pins this rule via a
|
|
9241
|
+
* grep over the admin route directory. If you intentionally land an admin
|
|
9242
|
+
* read/write route that targets only the JWT default org (no
|
|
9243
|
+
* cross-org switch awareness), update the whitelist there with a comment
|
|
9244
|
+
* explaining why.
|
|
9245
|
+
*/
|
|
9187
9246
|
function memberScope(request) {
|
|
9188
9247
|
const m = requireMember(request);
|
|
9189
9248
|
return {
|
|
@@ -9300,12 +9359,14 @@ async function requireMemberInOrg(db, request, orgId) {
|
|
|
9300
9359
|
const m = requireMember(request);
|
|
9301
9360
|
const [row] = await db.select({
|
|
9302
9361
|
id: members.id,
|
|
9303
|
-
role: members.role
|
|
9362
|
+
role: members.role,
|
|
9363
|
+
agentId: members.agentId
|
|
9304
9364
|
}).from(members).where(and(eq(members.userId, m.userId), eq(members.organizationId, orgId), eq(members.status, "active"))).limit(1);
|
|
9305
9365
|
if (!row) throw new ForbiddenError("Not an active member of the target organization");
|
|
9306
9366
|
return {
|
|
9307
9367
|
memberId: row.id,
|
|
9308
|
-
role: row.role
|
|
9368
|
+
role: row.role,
|
|
9369
|
+
agentId: row.agentId
|
|
9309
9370
|
};
|
|
9310
9371
|
}
|
|
9311
9372
|
/**
|
|
@@ -9330,6 +9391,7 @@ async function resolveAdminScope(db, request, scope, requestedOrganizationId) {
|
|
|
9330
9391
|
return {
|
|
9331
9392
|
...scope,
|
|
9332
9393
|
memberId: probe.memberId,
|
|
9394
|
+
humanAgentId: probe.agentId,
|
|
9333
9395
|
organizationId: requestedOrganizationId,
|
|
9334
9396
|
role: probe.role
|
|
9335
9397
|
};
|
|
@@ -11087,6 +11149,29 @@ async function listClientsForOrgAdmin(db, orgId) {
|
|
|
11087
11149
|
metadata: clients.metadata
|
|
11088
11150
|
}).from(clients).innerJoin(members, eq(members.userId, clients.userId)).where(and(eq(members.organizationId, orgId), eq(members.status, "active"))));
|
|
11089
11151
|
}
|
|
11152
|
+
/**
|
|
11153
|
+
* Infer whether the client's locally-cached refresh token can plausibly
|
|
11154
|
+
* still mint access tokens. Used by the Web admin dashboard to render an
|
|
11155
|
+
* "AUTH EXPIRED" pill on rows whose offline duration has exceeded the
|
|
11156
|
+
* server's configured refresh-token TTL.
|
|
11157
|
+
*
|
|
11158
|
+
* Uses `lastSeenAt` (not `connectedAt`) because a healthy long-lived
|
|
11159
|
+
* client slides the refresh token continuously, so the absolute connect
|
|
11160
|
+
* time is no proxy for liveness. `lastSeenAt` is updated on register,
|
|
11161
|
+
* heartbeat, and the final disconnect — it lower-bounds the issue time
|
|
11162
|
+
* of the refresh token the client most likely still holds.
|
|
11163
|
+
*
|
|
11164
|
+
* Pure function, no DB access; the column-less design means there's no
|
|
11165
|
+
* server-side revocation path yet — every "expired" decision is purely
|
|
11166
|
+
* time-based. If we ever want admin-driven revocation, add a column
|
|
11167
|
+
* back and OR its value into this function.
|
|
11168
|
+
*/
|
|
11169
|
+
function deriveAuthState(row, refreshTokenExpirySeconds) {
|
|
11170
|
+
if (row.status === "disconnected") {
|
|
11171
|
+
if (Date.now() - row.lastSeenAt.getTime() > refreshTokenExpirySeconds * 1e3) return "expired";
|
|
11172
|
+
}
|
|
11173
|
+
return "ok";
|
|
11174
|
+
}
|
|
11090
11175
|
async function attachAgentCounts(db, rows) {
|
|
11091
11176
|
const counts = await db.select({
|
|
11092
11177
|
clientId: agents.clientId,
|
|
@@ -12025,13 +12110,23 @@ async function adminAgentRoutes(app) {
|
|
|
12025
12110
|
connection
|
|
12026
12111
|
});
|
|
12027
12112
|
});
|
|
12028
|
-
/** POST /admin/agents/:uuid/chats — create a new workspace chat with the target agent
|
|
12113
|
+
/** POST /admin/agents/:uuid/chats — create a new workspace chat with the target agent.
|
|
12114
|
+
*
|
|
12115
|
+
* The chat creator is the user's HUMAN agent in the *target agent's* org,
|
|
12116
|
+
* not in the JWT default org. Otherwise a multi-org user creating a chat
|
|
12117
|
+
* with an agent outside their JWT default org gets `createChat`'s
|
|
12118
|
+
* cross-organization guard ("Cross-organization chat not allowed: …"),
|
|
12119
|
+
* even though the human is a member of the target agent's org.
|
|
12120
|
+
* Symptom hit by the inline onboarding flow when the user creates a new
|
|
12121
|
+
* agent in a non-default org and the auto-chat step fires.
|
|
12122
|
+
*/
|
|
12029
12123
|
app.post("/:uuid/chats", async (request, reply) => {
|
|
12030
12124
|
const { uuid: targetAgentId } = request.params;
|
|
12031
12125
|
const scope = memberScope(request);
|
|
12032
12126
|
await assertAgentVisible(app.db, scope, targetAgentId);
|
|
12033
|
-
const
|
|
12034
|
-
const
|
|
12127
|
+
const targetAgent = await getAgent(app.db, targetAgentId);
|
|
12128
|
+
const probe = await requireMemberInOrg(app.db, request, targetAgent.organizationId);
|
|
12129
|
+
const result = await createChat(app.db, probe.agentId, {
|
|
12035
12130
|
type: "direct",
|
|
12036
12131
|
participantIds: [targetAgentId]
|
|
12037
12132
|
});
|
|
@@ -12353,6 +12448,167 @@ async function adminChatRoutes(app) {
|
|
|
12353
12448
|
});
|
|
12354
12449
|
});
|
|
12355
12450
|
}
|
|
12451
|
+
/** In-memory set of consumed connect token JTIs. Entries auto-expire after 10 minutes. */
|
|
12452
|
+
const consumedConnectJtis = /* @__PURE__ */ new Map();
|
|
12453
|
+
const CONNECT_JTI_TTL_MS = 6e5;
|
|
12454
|
+
async function signToken(secret, payload, expiry) {
|
|
12455
|
+
return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setJti(randomUUID()).setExpirationTime(expiry).sign(secret);
|
|
12456
|
+
}
|
|
12457
|
+
/**
|
|
12458
|
+
* Convert an `ms`-style expiry string (e.g. `"30m"`, `"30d"`, `"1w"`) to
|
|
12459
|
+
* seconds. Used to surface the connect token's `expiresIn` in the API
|
|
12460
|
+
* response. Mirrors the subset of `jose.setExpirationTime` we use; falls
|
|
12461
|
+
* through with a clear error on malformed config so a typo in the env var
|
|
12462
|
+
* surfaces at boot, not days later when the first token expires.
|
|
12463
|
+
*/
|
|
12464
|
+
function expiryToSeconds(expiry) {
|
|
12465
|
+
const m = /^(\d+)\s*(s|m|h|d|w)$/.exec(expiry.trim());
|
|
12466
|
+
if (!m) throw new Error(`Invalid expiry "${expiry}" — expected forms like "30s", "10m", "2h", "30d", "1w".`);
|
|
12467
|
+
return Number(m[1]) * {
|
|
12468
|
+
s: 1,
|
|
12469
|
+
m: 60,
|
|
12470
|
+
h: 3600,
|
|
12471
|
+
d: 86400,
|
|
12472
|
+
w: 604800
|
|
12473
|
+
}[m[2]];
|
|
12474
|
+
}
|
|
12475
|
+
/**
|
|
12476
|
+
* Sign an `(access, refresh)` pair for the given member. Used by both the
|
|
12477
|
+
* legacy username/password login path and the SaaS GitHub OAuth callback,
|
|
12478
|
+
* so the issuance shape stays in one place.
|
|
12479
|
+
*/
|
|
12480
|
+
async function signTokensForMember(jwtSecretKey, member, expiries) {
|
|
12481
|
+
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
12482
|
+
const tokenBase = {
|
|
12483
|
+
sub: member.userId,
|
|
12484
|
+
memberId: member.memberId,
|
|
12485
|
+
organizationId: member.organizationId,
|
|
12486
|
+
role: member.role
|
|
12487
|
+
};
|
|
12488
|
+
return {
|
|
12489
|
+
accessToken: await signToken(secret, {
|
|
12490
|
+
...tokenBase,
|
|
12491
|
+
type: "access"
|
|
12492
|
+
}, expiries.accessTokenExpiry),
|
|
12493
|
+
refreshToken: await signToken(secret, {
|
|
12494
|
+
...tokenBase,
|
|
12495
|
+
type: "refresh"
|
|
12496
|
+
}, expiries.refreshTokenExpiry)
|
|
12497
|
+
};
|
|
12498
|
+
}
|
|
12499
|
+
async function login(db, username, password, jwtSecretKey, expiries) {
|
|
12500
|
+
const [user] = await db.select().from(users).where(eq(users.username, username)).limit(1);
|
|
12501
|
+
if (!user || user.status !== "active") throw new UnauthorizedError("Invalid username or password");
|
|
12502
|
+
if (!await bcrypt.compare(password, user.passwordHash)) throw new UnauthorizedError("Invalid username or password");
|
|
12503
|
+
const [member] = await db.select().from(members).where(and(eq(members.userId, user.id), eq(members.status, "active"))).orderBy(desc(members.createdAt), desc(members.id)).limit(1);
|
|
12504
|
+
if (!member) throw new UnauthorizedError("No organization membership found");
|
|
12505
|
+
const tokens = await signTokensForMember(jwtSecretKey, {
|
|
12506
|
+
userId: user.id,
|
|
12507
|
+
memberId: member.id,
|
|
12508
|
+
organizationId: member.organizationId,
|
|
12509
|
+
role: member.role
|
|
12510
|
+
}, expiries);
|
|
12511
|
+
await db.update(users).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq(users.id, user.id));
|
|
12512
|
+
return tokens;
|
|
12513
|
+
}
|
|
12514
|
+
/**
|
|
12515
|
+
* Refresh an access token. Sliding-window: the response also carries a
|
|
12516
|
+
* fresh refresh token whose lifetime restarts from now, so an actively-used
|
|
12517
|
+
* client never hits the absolute `refreshTokenExpiry`. The previously
|
|
12518
|
+
* issued refresh token remains valid until its own `exp` — we deliberately
|
|
12519
|
+
* do **not** maintain a server-side jti revocation set. Same-process
|
|
12520
|
+
* concurrent refreshes therefore both succeed; the surviving refresh token
|
|
12521
|
+
* is whichever one the client persists last. The alternative (server-side
|
|
12522
|
+
* invalidation) is more defensive against token theft but introduces races
|
|
12523
|
+
* across systemd-supervised restarts and reconnect storms — a tradeoff
|
|
12524
|
+
* we're not paying for here.
|
|
12525
|
+
*/
|
|
12526
|
+
async function refreshAccessToken(db, refreshToken, jwtSecretKey, expiries) {
|
|
12527
|
+
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
12528
|
+
let payload;
|
|
12529
|
+
try {
|
|
12530
|
+
const { payload: p } = await jwtVerify(refreshToken, secret);
|
|
12531
|
+
payload = p;
|
|
12532
|
+
} catch {
|
|
12533
|
+
throw new UnauthorizedError("Invalid or expired refresh token");
|
|
12534
|
+
}
|
|
12535
|
+
if (payload.type !== "refresh" || !payload.sub) throw new UnauthorizedError("Invalid token type");
|
|
12536
|
+
const [user] = await db.select({
|
|
12537
|
+
id: users.id,
|
|
12538
|
+
status: users.status
|
|
12539
|
+
}).from(users).where(eq(users.id, payload.sub)).limit(1);
|
|
12540
|
+
if (!user || user.status !== "active") throw new UnauthorizedError("User not found or suspended");
|
|
12541
|
+
const [member] = await db.select().from(members).where(and(eq(members.id, payload.memberId), eq(members.status, "active"))).limit(1);
|
|
12542
|
+
if (!member) throw new UnauthorizedError("Membership not found");
|
|
12543
|
+
return signTokensForMember(jwtSecretKey, {
|
|
12544
|
+
userId: user.id,
|
|
12545
|
+
memberId: member.id,
|
|
12546
|
+
organizationId: member.organizationId,
|
|
12547
|
+
role: member.role
|
|
12548
|
+
}, expiries);
|
|
12549
|
+
}
|
|
12550
|
+
/**
|
|
12551
|
+
* Generate a short-lived connect token for CLI authentication.
|
|
12552
|
+
* The connect token carries the member's identity and can be exchanged
|
|
12553
|
+
* for full access+refresh tokens via exchangeConnectToken().
|
|
12554
|
+
*
|
|
12555
|
+
* `iss` (when supplied) is stamped into the JWT so the CLI can derive
|
|
12556
|
+
* the hub URL with no additional argument. Production servers must
|
|
12557
|
+
* always pass it; dev callers may omit and the CLI will require an
|
|
12558
|
+
* explicit `--server-url` (legacy form).
|
|
12559
|
+
*/
|
|
12560
|
+
async function generateConnectToken(member, jwtSecretKey, expiries, iss) {
|
|
12561
|
+
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
12562
|
+
const jti = randomUUID();
|
|
12563
|
+
const builder = new SignJWT({
|
|
12564
|
+
sub: member.userId,
|
|
12565
|
+
memberId: member.memberId,
|
|
12566
|
+
organizationId: member.organizationId,
|
|
12567
|
+
role: member.role,
|
|
12568
|
+
type: "connect"
|
|
12569
|
+
}).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setJti(jti).setExpirationTime(expiries.connectTokenExpiry);
|
|
12570
|
+
if (iss) builder.setIssuer(iss);
|
|
12571
|
+
return {
|
|
12572
|
+
token: await builder.sign(secret),
|
|
12573
|
+
expiresIn: expiryToSeconds(expiries.connectTokenExpiry)
|
|
12574
|
+
};
|
|
12575
|
+
}
|
|
12576
|
+
/**
|
|
12577
|
+
* Exchange a connect token for full access+refresh tokens.
|
|
12578
|
+
* Validates the connect token, verifies the user is still active,
|
|
12579
|
+
* and issues a fresh token pair.
|
|
12580
|
+
*/
|
|
12581
|
+
async function exchangeConnectToken(db, connectToken, jwtSecretKey, expiries) {
|
|
12582
|
+
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
12583
|
+
let payload;
|
|
12584
|
+
try {
|
|
12585
|
+
const { payload: p } = await jwtVerify(connectToken, secret);
|
|
12586
|
+
payload = p;
|
|
12587
|
+
} catch {
|
|
12588
|
+
throw new UnauthorizedError("Invalid or expired connect token");
|
|
12589
|
+
}
|
|
12590
|
+
if (payload.type !== "connect" || !payload.sub || !payload.memberId) throw new UnauthorizedError("Invalid token type — expected connect token");
|
|
12591
|
+
const jti = payload.jti;
|
|
12592
|
+
if (jti) {
|
|
12593
|
+
if (consumedConnectJtis.has(jti)) throw new UnauthorizedError("Connect token has already been used");
|
|
12594
|
+
consumedConnectJtis.set(jti, Date.now());
|
|
12595
|
+
const cutoff = Date.now() - CONNECT_JTI_TTL_MS;
|
|
12596
|
+
for (const [k, ts] of consumedConnectJtis) if (ts < cutoff) consumedConnectJtis.delete(k);
|
|
12597
|
+
}
|
|
12598
|
+
const [user] = await db.select({
|
|
12599
|
+
id: users.id,
|
|
12600
|
+
status: users.status
|
|
12601
|
+
}).from(users).where(eq(users.id, payload.sub)).limit(1);
|
|
12602
|
+
if (!user || user.status !== "active") throw new UnauthorizedError("User not found or suspended");
|
|
12603
|
+
const [member] = await db.select().from(members).where(and(eq(members.id, payload.memberId), eq(members.status, "active"))).limit(1);
|
|
12604
|
+
if (!member) throw new UnauthorizedError("Membership not found");
|
|
12605
|
+
return signTokensForMember(jwtSecretKey, {
|
|
12606
|
+
userId: user.id,
|
|
12607
|
+
memberId: member.id,
|
|
12608
|
+
organizationId: member.organizationId,
|
|
12609
|
+
role: member.role
|
|
12610
|
+
}, expiries);
|
|
12611
|
+
}
|
|
12356
12612
|
/** Serialize a Date to ISO string, or null. */
|
|
12357
12613
|
function serializeDate(d) {
|
|
12358
12614
|
return d ? d.toISOString() : null;
|
|
@@ -12362,13 +12618,16 @@ async function adminClientRoutes(app) {
|
|
|
12362
12618
|
app.get("/", async (request) => {
|
|
12363
12619
|
const scope = memberScope(request);
|
|
12364
12620
|
const { organizationId } = listClientsQuerySchema.parse(request.query);
|
|
12365
|
-
|
|
12621
|
+
const clients = organizationId ? await (async () => {
|
|
12366
12622
|
if ((await requireMemberInOrg(app.db, request, organizationId)).role !== "admin") throw new ForbiddenError("Admin role required");
|
|
12367
12623
|
return listClientsForOrgAdmin(app.db, organizationId);
|
|
12368
|
-
})() : await listClients(app.db, { userId: scope.userId })
|
|
12624
|
+
})() : await listClients(app.db, { userId: scope.userId });
|
|
12625
|
+
const refreshExpirySeconds = expiryToSeconds(app.config.auth.refreshTokenExpiry);
|
|
12626
|
+
return clients.map((c) => ({
|
|
12369
12627
|
id: c.id,
|
|
12370
12628
|
userId: c.userId,
|
|
12371
12629
|
status: c.status,
|
|
12630
|
+
authState: deriveAuthState(c, refreshExpirySeconds),
|
|
12372
12631
|
sdkVersion: c.sdkVersion,
|
|
12373
12632
|
hostname: c.hostname,
|
|
12374
12633
|
os: c.os,
|
|
@@ -12395,10 +12654,12 @@ async function adminClientRoutes(app) {
|
|
|
12395
12654
|
if (!client) throw new Error("unreachable: client missing after owner check");
|
|
12396
12655
|
const metadata = client.metadata ?? {};
|
|
12397
12656
|
const capabilities = metadata.capabilities && typeof metadata.capabilities === "object" ? metadata.capabilities : {};
|
|
12657
|
+
const refreshExpirySeconds = expiryToSeconds(app.config.auth.refreshTokenExpiry);
|
|
12398
12658
|
return {
|
|
12399
12659
|
id: client.id,
|
|
12400
12660
|
userId: client.userId,
|
|
12401
12661
|
status: client.status,
|
|
12662
|
+
authState: deriveAuthState(client, refreshExpirySeconds),
|
|
12402
12663
|
sdkVersion: client.sdkVersion,
|
|
12403
12664
|
hostname: client.hostname,
|
|
12404
12665
|
os: client.os,
|
|
@@ -12428,9 +12689,12 @@ async function adminClientRoutes(app) {
|
|
|
12428
12689
|
return reply.status(204).send();
|
|
12429
12690
|
});
|
|
12430
12691
|
}
|
|
12692
|
+
const activityQuerySchema = z.object({ organizationId: z.string().min(1).optional() });
|
|
12431
12693
|
async function adminActivityRoutes(app) {
|
|
12432
12694
|
app.get("/", async (request) => {
|
|
12433
|
-
const
|
|
12695
|
+
const baseScope = memberScope(request);
|
|
12696
|
+
const { organizationId } = activityQuerySchema.parse(request.query);
|
|
12697
|
+
const scope = await resolveAdminScope(app.db, request, baseScope, organizationId);
|
|
12434
12698
|
const overview = await getActivityOverview(app.db);
|
|
12435
12699
|
const runningAgents = await listAgentsWithRuntime(app.db, scope);
|
|
12436
12700
|
return {
|
|
@@ -15326,141 +15590,6 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
15326
15590
|
});
|
|
15327
15591
|
};
|
|
15328
15592
|
}
|
|
15329
|
-
const ACCESS_TOKEN_EXPIRY = "30m";
|
|
15330
|
-
const REFRESH_TOKEN_EXPIRY = "7d";
|
|
15331
|
-
const CONNECT_TOKEN_EXPIRY = "10m";
|
|
15332
|
-
/** In-memory set of consumed connect token JTIs. Entries auto-expire after 10 minutes. */
|
|
15333
|
-
const consumedConnectJtis = /* @__PURE__ */ new Map();
|
|
15334
|
-
const CONNECT_JTI_TTL_MS = 6e5;
|
|
15335
|
-
async function signToken(secret, payload, expiry) {
|
|
15336
|
-
return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiry).sign(secret);
|
|
15337
|
-
}
|
|
15338
|
-
/**
|
|
15339
|
-
* Sign an `(access, refresh)` pair for the given member. Used by both the
|
|
15340
|
-
* legacy username/password login path and the SaaS GitHub OAuth callback,
|
|
15341
|
-
* so the issuance shape stays in one place.
|
|
15342
|
-
*/
|
|
15343
|
-
async function signTokensForMember(jwtSecretKey, member) {
|
|
15344
|
-
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
15345
|
-
const tokenBase = {
|
|
15346
|
-
sub: member.userId,
|
|
15347
|
-
memberId: member.memberId,
|
|
15348
|
-
organizationId: member.organizationId,
|
|
15349
|
-
role: member.role
|
|
15350
|
-
};
|
|
15351
|
-
return {
|
|
15352
|
-
accessToken: await signToken(secret, {
|
|
15353
|
-
...tokenBase,
|
|
15354
|
-
type: "access"
|
|
15355
|
-
}, ACCESS_TOKEN_EXPIRY),
|
|
15356
|
-
refreshToken: await signToken(secret, {
|
|
15357
|
-
...tokenBase,
|
|
15358
|
-
type: "refresh"
|
|
15359
|
-
}, REFRESH_TOKEN_EXPIRY)
|
|
15360
|
-
};
|
|
15361
|
-
}
|
|
15362
|
-
async function login(db, username, password, jwtSecretKey) {
|
|
15363
|
-
const [user] = await db.select().from(users).where(eq(users.username, username)).limit(1);
|
|
15364
|
-
if (!user || user.status !== "active") throw new UnauthorizedError("Invalid username or password");
|
|
15365
|
-
if (!await bcrypt.compare(password, user.passwordHash)) throw new UnauthorizedError("Invalid username or password");
|
|
15366
|
-
const [member] = await db.select().from(members).where(and(eq(members.userId, user.id), eq(members.status, "active"))).orderBy(desc(members.createdAt), desc(members.id)).limit(1);
|
|
15367
|
-
if (!member) throw new UnauthorizedError("No organization membership found");
|
|
15368
|
-
const tokens = await signTokensForMember(jwtSecretKey, {
|
|
15369
|
-
userId: user.id,
|
|
15370
|
-
memberId: member.id,
|
|
15371
|
-
organizationId: member.organizationId,
|
|
15372
|
-
role: member.role
|
|
15373
|
-
});
|
|
15374
|
-
await db.update(users).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq(users.id, user.id));
|
|
15375
|
-
return tokens;
|
|
15376
|
-
}
|
|
15377
|
-
async function refreshAccessToken(db, refreshToken, jwtSecretKey) {
|
|
15378
|
-
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
15379
|
-
let payload;
|
|
15380
|
-
try {
|
|
15381
|
-
const { payload: p } = await jwtVerify(refreshToken, secret);
|
|
15382
|
-
payload = p;
|
|
15383
|
-
} catch {
|
|
15384
|
-
throw new UnauthorizedError("Invalid or expired refresh token");
|
|
15385
|
-
}
|
|
15386
|
-
if (payload.type !== "refresh" || !payload.sub) throw new UnauthorizedError("Invalid token type");
|
|
15387
|
-
const [user] = await db.select({
|
|
15388
|
-
id: users.id,
|
|
15389
|
-
status: users.status
|
|
15390
|
-
}).from(users).where(eq(users.id, payload.sub)).limit(1);
|
|
15391
|
-
if (!user || user.status !== "active") throw new UnauthorizedError("User not found or suspended");
|
|
15392
|
-
const [member] = await db.select().from(members).where(and(eq(members.id, payload.memberId), eq(members.status, "active"))).limit(1);
|
|
15393
|
-
if (!member) throw new UnauthorizedError("Membership not found");
|
|
15394
|
-
return { accessToken: await signToken(secret, {
|
|
15395
|
-
sub: user.id,
|
|
15396
|
-
memberId: member.id,
|
|
15397
|
-
organizationId: member.organizationId,
|
|
15398
|
-
role: member.role,
|
|
15399
|
-
type: "access"
|
|
15400
|
-
}, ACCESS_TOKEN_EXPIRY) };
|
|
15401
|
-
}
|
|
15402
|
-
/**
|
|
15403
|
-
* Generate a short-lived connect token for CLI authentication.
|
|
15404
|
-
* The connect token carries the member's identity and can be exchanged
|
|
15405
|
-
* for full access+refresh tokens via exchangeConnectToken().
|
|
15406
|
-
*
|
|
15407
|
-
* `iss` (when supplied) is stamped into the JWT so the CLI can derive
|
|
15408
|
-
* the hub URL with no additional argument. Production servers must
|
|
15409
|
-
* always pass it; dev callers may omit and the CLI will require an
|
|
15410
|
-
* explicit `--server-url` (legacy form).
|
|
15411
|
-
*/
|
|
15412
|
-
async function generateConnectToken(member, jwtSecretKey, iss) {
|
|
15413
|
-
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
15414
|
-
const jti = randomUUID();
|
|
15415
|
-
const builder = new SignJWT({
|
|
15416
|
-
sub: member.userId,
|
|
15417
|
-
memberId: member.memberId,
|
|
15418
|
-
organizationId: member.organizationId,
|
|
15419
|
-
role: member.role,
|
|
15420
|
-
type: "connect"
|
|
15421
|
-
}).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setJti(jti).setExpirationTime(CONNECT_TOKEN_EXPIRY);
|
|
15422
|
-
if (iss) builder.setIssuer(iss);
|
|
15423
|
-
return {
|
|
15424
|
-
token: await builder.sign(secret),
|
|
15425
|
-
expiresIn: 600
|
|
15426
|
-
};
|
|
15427
|
-
}
|
|
15428
|
-
/**
|
|
15429
|
-
* Exchange a connect token for full access+refresh tokens.
|
|
15430
|
-
* Validates the connect token, verifies the user is still active,
|
|
15431
|
-
* and issues a fresh token pair.
|
|
15432
|
-
*/
|
|
15433
|
-
async function exchangeConnectToken(db, connectToken, jwtSecretKey) {
|
|
15434
|
-
const secret = new TextEncoder().encode(jwtSecretKey);
|
|
15435
|
-
let payload;
|
|
15436
|
-
try {
|
|
15437
|
-
const { payload: p } = await jwtVerify(connectToken, secret);
|
|
15438
|
-
payload = p;
|
|
15439
|
-
} catch {
|
|
15440
|
-
throw new UnauthorizedError("Invalid or expired connect token");
|
|
15441
|
-
}
|
|
15442
|
-
if (payload.type !== "connect" || !payload.sub || !payload.memberId) throw new UnauthorizedError("Invalid token type — expected connect token");
|
|
15443
|
-
const jti = payload.jti;
|
|
15444
|
-
if (jti) {
|
|
15445
|
-
if (consumedConnectJtis.has(jti)) throw new UnauthorizedError("Connect token has already been used");
|
|
15446
|
-
consumedConnectJtis.set(jti, Date.now());
|
|
15447
|
-
const cutoff = Date.now() - CONNECT_JTI_TTL_MS;
|
|
15448
|
-
for (const [k, ts] of consumedConnectJtis) if (ts < cutoff) consumedConnectJtis.delete(k);
|
|
15449
|
-
}
|
|
15450
|
-
const [user] = await db.select({
|
|
15451
|
-
id: users.id,
|
|
15452
|
-
status: users.status
|
|
15453
|
-
}).from(users).where(eq(users.id, payload.sub)).limit(1);
|
|
15454
|
-
if (!user || user.status !== "active") throw new UnauthorizedError("User not found or suspended");
|
|
15455
|
-
const [member] = await db.select().from(members).where(and(eq(members.id, payload.memberId), eq(members.status, "active"))).limit(1);
|
|
15456
|
-
if (!member) throw new UnauthorizedError("Membership not found");
|
|
15457
|
-
return signTokensForMember(jwtSecretKey, {
|
|
15458
|
-
userId: user.id,
|
|
15459
|
-
memberId: member.id,
|
|
15460
|
-
organizationId: member.organizationId,
|
|
15461
|
-
role: member.role
|
|
15462
|
-
});
|
|
15463
|
-
}
|
|
15464
15593
|
/**
|
|
15465
15594
|
* Third-party / local auth identities for a user. Models "how does this user
|
|
15466
15595
|
* prove they are who they say they are". A single user MAY have multiple
|
|
@@ -16045,7 +16174,7 @@ async function completeOauthFlow(app, request, reply, profile, next) {
|
|
|
16045
16174
|
memberId: memberInfo.memberId,
|
|
16046
16175
|
organizationId: memberInfo.organizationId,
|
|
16047
16176
|
role: memberInfo.role
|
|
16048
|
-
});
|
|
16177
|
+
}, app.config.auth);
|
|
16049
16178
|
const fragment = new URLSearchParams({
|
|
16050
16179
|
access: tokens.accessToken,
|
|
16051
16180
|
refresh: tokens.refreshToken,
|
|
@@ -16061,7 +16190,7 @@ async function authRoutes(app) {
|
|
|
16061
16190
|
timeWindow: "1 minute"
|
|
16062
16191
|
} } }, async (request, reply) => {
|
|
16063
16192
|
const body = loginSchema.parse(request.body);
|
|
16064
|
-
const result = await login(app.db, body.username, body.password, app.config.secrets.jwtSecret);
|
|
16193
|
+
const result = await login(app.db, body.username, body.password, app.config.secrets.jwtSecret, app.config.auth);
|
|
16065
16194
|
return reply.send(result);
|
|
16066
16195
|
});
|
|
16067
16196
|
app.post("/refresh", { config: { rateLimit: {
|
|
@@ -16069,7 +16198,7 @@ async function authRoutes(app) {
|
|
|
16069
16198
|
timeWindow: "1 minute"
|
|
16070
16199
|
} } }, async (request, reply) => {
|
|
16071
16200
|
const body = refreshTokenSchema.parse(request.body);
|
|
16072
|
-
const result = await refreshAccessToken(app.db, body.refreshToken, app.config.secrets.jwtSecret);
|
|
16201
|
+
const result = await refreshAccessToken(app.db, body.refreshToken, app.config.secrets.jwtSecret, app.config.auth);
|
|
16073
16202
|
return reply.send(result);
|
|
16074
16203
|
});
|
|
16075
16204
|
app.post("/connect-token", { config: { rateLimit: {
|
|
@@ -16077,7 +16206,7 @@ async function authRoutes(app) {
|
|
|
16077
16206
|
timeWindow: "1 minute"
|
|
16078
16207
|
} } }, async (request, reply) => {
|
|
16079
16208
|
const body = connectTokenExchangeSchema.parse(request.body);
|
|
16080
|
-
const result = await exchangeConnectToken(app.db, body.token, app.config.secrets.jwtSecret);
|
|
16209
|
+
const result = await exchangeConnectToken(app.db, body.token, app.config.secrets.jwtSecret, app.config.auth);
|
|
16081
16210
|
return reply.send(result);
|
|
16082
16211
|
});
|
|
16083
16212
|
}
|
|
@@ -16288,7 +16417,7 @@ async function meRoutes(app) {
|
|
|
16288
16417
|
memberId: m.memberId,
|
|
16289
16418
|
organizationId: m.organizationId,
|
|
16290
16419
|
role: m.role
|
|
16291
|
-
}, app.config.secrets.jwtSecret, issuer);
|
|
16420
|
+
}, app.config.secrets.jwtSecret, app.config.auth, issuer);
|
|
16292
16421
|
return {
|
|
16293
16422
|
token,
|
|
16294
16423
|
expiresIn,
|
|
@@ -16338,7 +16467,7 @@ async function meRoutes(app) {
|
|
|
16338
16467
|
memberId: created.memberId,
|
|
16339
16468
|
organizationId: created.organizationId,
|
|
16340
16469
|
role: "admin"
|
|
16341
|
-
});
|
|
16470
|
+
}, app.config.auth);
|
|
16342
16471
|
return reply.status(201).send({
|
|
16343
16472
|
organization: {
|
|
16344
16473
|
id: created.organizationId,
|
|
@@ -16380,7 +16509,7 @@ async function meRoutes(app) {
|
|
|
16380
16509
|
memberId: member.id,
|
|
16381
16510
|
organizationId: member.organizationId,
|
|
16382
16511
|
role: member.role
|
|
16383
|
-
});
|
|
16512
|
+
}, app.config.auth);
|
|
16384
16513
|
return reply.status(200).send({
|
|
16385
16514
|
organizationId: member.organizationId,
|
|
16386
16515
|
memberId: member.id,
|
|
@@ -16426,7 +16555,7 @@ async function inferWizardStep(app, m) {
|
|
|
16426
16555
|
* landing page.
|
|
16427
16556
|
*/
|
|
16428
16557
|
async function publicInvitePreviewRoute(app) {
|
|
16429
|
-
const { previewInvitation } = await import("./invitation-CBnQyB7o-
|
|
16558
|
+
const { previewInvitation } = await import("./invitation-CBnQyB7o-Bulf3Sl7.mjs");
|
|
16430
16559
|
app.get("/:token/preview", async (request, reply) => {
|
|
16431
16560
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16432
16561
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -16456,7 +16585,7 @@ async function adminInvitationRoutes(app) {
|
|
|
16456
16585
|
const m = requireMember(request);
|
|
16457
16586
|
if (m.role !== "admin") throw new ForbiddenError("Admin role required");
|
|
16458
16587
|
if (request.params.id !== m.organizationId) throw new ForbiddenError("Cannot rotate invitations for another organization");
|
|
16459
|
-
const { rotateInvitation } = await import("./invitation-CBnQyB7o-
|
|
16588
|
+
const { rotateInvitation } = await import("./invitation-CBnQyB7o-Bulf3Sl7.mjs");
|
|
16460
16589
|
const inv = await rotateInvitation(app.db, m.organizationId, m.userId);
|
|
16461
16590
|
return {
|
|
16462
16591
|
id: inv.id,
|
|
@@ -18331,6 +18460,14 @@ function resolveCommandVersion$1(injected) {
|
|
|
18331
18460
|
return "0.0.0";
|
|
18332
18461
|
}
|
|
18333
18462
|
async function buildApp(config) {
|
|
18463
|
+
try {
|
|
18464
|
+
expiryToSeconds(config.auth.accessTokenExpiry);
|
|
18465
|
+
expiryToSeconds(config.auth.refreshTokenExpiry);
|
|
18466
|
+
expiryToSeconds(config.auth.connectTokenExpiry);
|
|
18467
|
+
} catch (err) {
|
|
18468
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18469
|
+
throw new Error(`${msg} — check FIRST_TREE_HUB_AUTH_*_EXPIRY env vars (got access=${config.auth.accessTokenExpiry}, refresh=${config.auth.refreshTokenExpiry}, connect=${config.auth.connectTokenExpiry}).`);
|
|
18470
|
+
}
|
|
18334
18471
|
applyLoggerConfig({
|
|
18335
18472
|
level: config.observability.logging.level,
|
|
18336
18473
|
format: config.observability.logging.format,
|