@kb-labs/mcp-app 2.119.1-canary.ee03bc25d → 2.120.0

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/bin.cjs CHANGED
@@ -87235,7 +87235,7 @@ var SignJWT = class extends ProduceJWT {
87235
87235
  };
87236
87236
 
87237
87237
  // ../../gateway/auth/dist/index.js
87238
- __toESM(require_bcryptjs());
87238
+ var import_bcryptjs = __toESM(require_bcryptjs());
87239
87239
  init_zod();
87240
87240
 
87241
87241
  // ../../../core/contracts/dist/index.js
@@ -87784,6 +87784,549 @@ var AuthService = class {
87784
87784
  };
87785
87785
  }
87786
87786
  };
87787
+ var COLLECTION = "users";
87788
+ var canonicalizeEmail = (raw) => raw.trim().toLowerCase();
87789
+ var docToUser = (doc) => ({
87790
+ userId: doc.userId,
87791
+ tenantId: doc.tenantId,
87792
+ email: doc.email,
87793
+ displayName: doc.displayName,
87794
+ status: doc.status,
87795
+ createdAt: doc.createdAt,
87796
+ updatedAt: doc.updatedAt
87797
+ });
87798
+ var UsersStore = class {
87799
+ constructor(docs) {
87800
+ this.docs = docs;
87801
+ }
87802
+ docs;
87803
+ initialised = null;
87804
+ async ensureSchema() {
87805
+ if (!this.initialised) {
87806
+ this.initialised = (async () => {
87807
+ await this.docs.ensureCollection(COLLECTION, {
87808
+ indexes: [
87809
+ // Logical primary key for "user inside tenant" lookups.
87810
+ { path: ["tenantId", "email"], unique: true },
87811
+ // userId is the surrogate key callers reference everywhere.
87812
+ { path: "userId", unique: true },
87813
+ { path: "tenantId" }
87814
+ ]
87815
+ });
87816
+ })();
87817
+ }
87818
+ await this.initialised;
87819
+ }
87820
+ async create(input) {
87821
+ await this.ensureSchema();
87822
+ const email3 = canonicalizeEmail(input.email);
87823
+ await this.docs.insertOne(COLLECTION, {
87824
+ userId: input.userId,
87825
+ tenantId: input.tenantId,
87826
+ email: email3,
87827
+ displayName: input.displayName,
87828
+ status: input.status
87829
+ });
87830
+ const created = await this.getById(input.userId);
87831
+ if (!created) {
87832
+ throw new Error(`users-store: inserted user ${input.userId} but read-back returned null`);
87833
+ }
87834
+ return created;
87835
+ }
87836
+ async getById(userId) {
87837
+ await this.ensureSchema();
87838
+ const doc = await this.docs.findById(COLLECTION, userId);
87839
+ if (doc && doc.userId === userId) {
87840
+ return docToUser(doc);
87841
+ }
87842
+ const [byField] = await this.docs.find(
87843
+ COLLECTION,
87844
+ { userId: { $eq: userId } },
87845
+ { limit: 1 }
87846
+ );
87847
+ return byField ? docToUser(byField) : null;
87848
+ }
87849
+ async findByEmailTenant(email3, tenantId) {
87850
+ await this.ensureSchema();
87851
+ const normalised = canonicalizeEmail(email3);
87852
+ const [doc] = await this.docs.find(
87853
+ COLLECTION,
87854
+ { $and: [{ tenantId: { $eq: tenantId } }, { email: { $eq: normalised } }] },
87855
+ { limit: 1 }
87856
+ );
87857
+ return doc ? docToUser(doc) : null;
87858
+ }
87859
+ async setStatus(userId, status) {
87860
+ await this.ensureSchema();
87861
+ const existing = await this.getById(userId);
87862
+ if (!existing) {
87863
+ throw new Error(`users-store: setStatus on unknown userId=${userId}`);
87864
+ }
87865
+ await this.docs.updateMany(
87866
+ COLLECTION,
87867
+ { userId: { $eq: userId } },
87868
+ { $set: { status } }
87869
+ );
87870
+ }
87871
+ /** List all users for a given tenant. */
87872
+ async listByTenant(tenantId) {
87873
+ await this.ensureSchema();
87874
+ const docs = await this.docs.find(
87875
+ COLLECTION,
87876
+ { tenantId: { $eq: tenantId } }
87877
+ );
87878
+ return docs.map(docToUser);
87879
+ }
87880
+ async delete(userId) {
87881
+ await this.ensureSchema();
87882
+ await this.docs.deleteMany(COLLECTION, { userId: { $eq: userId } });
87883
+ }
87884
+ };
87885
+ var COLLECTION2 = "credentials";
87886
+ var docToCredential = (doc) => ({
87887
+ userId: doc.userId,
87888
+ providerId: doc.providerId,
87889
+ hash: doc.hash,
87890
+ createdAt: doc.createdAt,
87891
+ updatedAt: doc.updatedAt
87892
+ });
87893
+ var CredentialsStore = class {
87894
+ constructor(docs) {
87895
+ this.docs = docs;
87896
+ }
87897
+ docs;
87898
+ initialised = null;
87899
+ async ensureSchema() {
87900
+ if (!this.initialised) {
87901
+ this.initialised = (async () => {
87902
+ await this.docs.ensureCollection(COLLECTION2, {
87903
+ indexes: [
87904
+ // Logical primary key.
87905
+ { path: ["userId", "providerId"], unique: true },
87906
+ // Cascade delete needs userId alone.
87907
+ { path: "userId" }
87908
+ ]
87909
+ });
87910
+ })();
87911
+ }
87912
+ await this.initialised;
87913
+ }
87914
+ async setCredential(input) {
87915
+ await this.ensureSchema();
87916
+ await this.docs.updateOne(
87917
+ COLLECTION2,
87918
+ {
87919
+ $and: [
87920
+ { userId: { $eq: input.userId } },
87921
+ { providerId: { $eq: input.providerId } }
87922
+ ]
87923
+ },
87924
+ {
87925
+ $set: {
87926
+ userId: input.userId,
87927
+ providerId: input.providerId,
87928
+ hash: input.hash
87929
+ }
87930
+ },
87931
+ { upsert: true }
87932
+ );
87933
+ }
87934
+ async getCredential(userId, providerId) {
87935
+ await this.ensureSchema();
87936
+ const [doc] = await this.docs.find(
87937
+ COLLECTION2,
87938
+ {
87939
+ $and: [
87940
+ { userId: { $eq: userId } },
87941
+ { providerId: { $eq: providerId } }
87942
+ ]
87943
+ },
87944
+ { limit: 1 }
87945
+ );
87946
+ return doc ? docToCredential(doc) : null;
87947
+ }
87948
+ async deleteCredential(userId, providerId) {
87949
+ await this.ensureSchema();
87950
+ await this.docs.deleteMany(COLLECTION2, {
87951
+ $and: [
87952
+ { userId: { $eq: userId } },
87953
+ { providerId: { $eq: providerId } }
87954
+ ]
87955
+ });
87956
+ }
87957
+ /**
87958
+ * Cascade removal helper — call from the parent flow when a `User` is
87959
+ * deleted so we never leave dangling credentials in the store.
87960
+ */
87961
+ async deleteAllForUser(userId) {
87962
+ await this.ensureSchema();
87963
+ await this.docs.deleteMany(COLLECTION2, {
87964
+ userId: { $eq: userId }
87965
+ });
87966
+ }
87967
+ };
87968
+ var COLLECTION3 = "memberships";
87969
+ var docToMembership = (doc) => ({
87970
+ userId: doc.userId,
87971
+ tenantId: doc.tenantId,
87972
+ groupId: doc.groupId,
87973
+ createdAt: doc.createdAt,
87974
+ updatedAt: doc.updatedAt
87975
+ });
87976
+ var MembershipsStore = class {
87977
+ constructor(docs) {
87978
+ this.docs = docs;
87979
+ }
87980
+ docs;
87981
+ initialised = null;
87982
+ async ensureSchema() {
87983
+ if (!this.initialised) {
87984
+ this.initialised = (async () => {
87985
+ await this.docs.ensureCollection(COLLECTION3, {
87986
+ indexes: [
87987
+ { path: ["userId", "tenantId"], unique: true },
87988
+ { path: "userId" },
87989
+ { path: "tenantId" }
87990
+ ]
87991
+ });
87992
+ })();
87993
+ }
87994
+ await this.initialised;
87995
+ }
87996
+ async addMembership(input) {
87997
+ await this.ensureSchema();
87998
+ await this.docs.insertOne(COLLECTION3, {
87999
+ userId: input.userId,
88000
+ tenantId: input.tenantId,
88001
+ groupId: input.groupId
88002
+ });
88003
+ }
88004
+ async setGroup(userId, tenantId, groupId) {
88005
+ await this.ensureSchema();
88006
+ const [existing] = await this.docs.find(
88007
+ COLLECTION3,
88008
+ { $and: [{ userId: { $eq: userId } }, { tenantId: { $eq: tenantId } }] },
88009
+ { limit: 1 }
88010
+ );
88011
+ if (!existing) {
88012
+ throw new Error(
88013
+ `memberships-store: setGroup on missing membership userId=${userId} tenantId=${tenantId}`
88014
+ );
88015
+ }
88016
+ await this.docs.updateMany(
88017
+ COLLECTION3,
88018
+ { $and: [{ userId: { $eq: userId } }, { tenantId: { $eq: tenantId } }] },
88019
+ { $set: { groupId } }
88020
+ );
88021
+ }
88022
+ async listByUser(userId) {
88023
+ await this.ensureSchema();
88024
+ const docs = await this.docs.find(
88025
+ COLLECTION3,
88026
+ { userId: { $eq: userId } },
88027
+ { sort: { tenantId: 1 } }
88028
+ );
88029
+ return docs.map(docToMembership);
88030
+ }
88031
+ async listByTenant(tenantId) {
88032
+ await this.ensureSchema();
88033
+ const docs = await this.docs.find(
88034
+ COLLECTION3,
88035
+ { tenantId: { $eq: tenantId } },
88036
+ { sort: { userId: 1 } }
88037
+ );
88038
+ return docs.map(docToMembership);
88039
+ }
88040
+ async removeMembership(userId, tenantId) {
88041
+ await this.ensureSchema();
88042
+ await this.docs.deleteMany(
88043
+ COLLECTION3,
88044
+ { $and: [{ userId: { $eq: userId } }, { tenantId: { $eq: tenantId } }] }
88045
+ );
88046
+ }
88047
+ /** Cascade hook — call when a `User` is deleted. */
88048
+ async removeAllForUser(userId) {
88049
+ await this.ensureSchema();
88050
+ await this.docs.deleteMany(COLLECTION3, { userId: { $eq: userId } });
88051
+ }
88052
+ };
88053
+ var FAMILIES = "session_families";
88054
+ var REFRESHES = "refresh_tokens";
88055
+ var RefreshNotFoundError = class extends Error {
88056
+ constructor() {
88057
+ super("refresh token not found");
88058
+ this.name = "RefreshNotFoundError";
88059
+ }
88060
+ };
88061
+ var RefreshExpiredError = class extends Error {
88062
+ constructor() {
88063
+ super("refresh token expired");
88064
+ this.name = "RefreshExpiredError";
88065
+ }
88066
+ };
88067
+ var RefreshReuseDetectedError = class extends Error {
88068
+ constructor(familyId) {
88069
+ super(`refresh token reuse detected (family ${familyId} killed)`);
88070
+ this.familyId = familyId;
88071
+ this.name = "RefreshReuseDetectedError";
88072
+ }
88073
+ familyId;
88074
+ };
88075
+ var newId = (bytes = 16) => crypto4.randomBytes(bytes).toString("base64url");
88076
+ var SessionsStore = class {
88077
+ constructor(docs, opts) {
88078
+ this.docs = docs;
88079
+ this.now = opts.now ?? Date.now;
88080
+ this.refreshTtlMs = opts.refreshTtlMs;
88081
+ this.graceWindowMs = opts.graceWindowMs;
88082
+ }
88083
+ docs;
88084
+ initialised = null;
88085
+ now;
88086
+ refreshTtlMs;
88087
+ graceWindowMs;
88088
+ async ensureSchema() {
88089
+ if (!this.initialised) {
88090
+ this.initialised = (async () => {
88091
+ await this.docs.ensureCollection(FAMILIES, {
88092
+ indexes: [
88093
+ { path: "familyId", unique: true },
88094
+ { path: "userId" }
88095
+ ]
88096
+ });
88097
+ await this.docs.ensureCollection(REFRESHES, {
88098
+ indexes: [
88099
+ { path: "jti", unique: true },
88100
+ { path: "familyId" },
88101
+ { path: "userId" },
88102
+ // Best-effort sweep; `rotateRefresh` checks expiresAt > now (CD-9).
88103
+ { path: "expiresAt", ttl: 0 }
88104
+ ]
88105
+ });
88106
+ })();
88107
+ }
88108
+ await this.initialised;
88109
+ }
88110
+ async createSession(input) {
88111
+ await this.ensureSchema();
88112
+ const familyId = newId();
88113
+ const refreshJti = newId();
88114
+ const now = this.now();
88115
+ const refreshExpiresAt = now + this.refreshTtlMs;
88116
+ await this.docs.transaction(async (tx) => {
88117
+ await tx.insertOne(FAMILIES, {
88118
+ familyId,
88119
+ userId: input.userId,
88120
+ tenantId: input.tenantId,
88121
+ lastUsedAt: now,
88122
+ userAgent: input.deviceCtx.userAgent,
88123
+ ipFirst: input.deviceCtx.ip
88124
+ });
88125
+ await tx.insertOne(REFRESHES, {
88126
+ jti: refreshJti,
88127
+ familyId,
88128
+ userId: input.userId,
88129
+ expiresAt: refreshExpiresAt
88130
+ });
88131
+ });
88132
+ return { familyId, refreshJti, refreshExpiresAt };
88133
+ }
88134
+ /**
88135
+ * Consume `oldJti` and issue a new refresh in the same family.
88136
+ *
88137
+ * Throws:
88138
+ * - `RefreshNotFoundError` if jti is unknown or its family was revoked.
88139
+ * - `RefreshExpiredError` if jti is past `expiresAt` (family stays alive).
88140
+ * - `RefreshReuseDetectedError` if jti was already consumed and the
88141
+ * grace window has passed — family is killed before throwing.
88142
+ *
88143
+ * On grace-window retry (same already-consumed jti within
88144
+ * `graceWindowMs`) the previously-issued replacement is returned and
88145
+ * no new refresh is created.
88146
+ */
88147
+ async rotateRefresh(oldJti) {
88148
+ await this.ensureSchema();
88149
+ const now = this.now();
88150
+ const newJti = newId();
88151
+ const newExpiresAt = now + this.refreshTtlMs;
88152
+ const outcome = await this.docs.transaction(async (tx) => {
88153
+ const [current] = await tx.find(
88154
+ REFRESHES,
88155
+ { jti: { $eq: oldJti } },
88156
+ { limit: 1 }
88157
+ );
88158
+ if (!current) {
88159
+ return { tag: "not-found" };
88160
+ }
88161
+ if (current.consumedAt !== void 0) {
88162
+ const elapsed = now - current.consumedAt;
88163
+ if (elapsed <= this.graceWindowMs && current.replacedBy) {
88164
+ const [replacement] = await tx.find(
88165
+ REFRESHES,
88166
+ { jti: { $eq: current.replacedBy } },
88167
+ { limit: 1 }
88168
+ );
88169
+ if (replacement) {
88170
+ return {
88171
+ tag: "ok",
88172
+ result: {
88173
+ newJti: replacement.jti,
88174
+ familyId: current.familyId,
88175
+ newRefreshExpiresAt: replacement.expiresAt
88176
+ }
88177
+ };
88178
+ }
88179
+ }
88180
+ if (current.expiresAt <= now) {
88181
+ return { tag: "expired" };
88182
+ }
88183
+ return { tag: "reuse", familyId: current.familyId };
88184
+ }
88185
+ if (current.expiresAt <= now) {
88186
+ return { tag: "expired" };
88187
+ }
88188
+ await tx.updateMany(
88189
+ REFRESHES,
88190
+ { jti: { $eq: oldJti } },
88191
+ { $set: { consumedAt: now, replacedBy: newJti } }
88192
+ );
88193
+ await tx.insertOne(REFRESHES, {
88194
+ jti: newJti,
88195
+ familyId: current.familyId,
88196
+ userId: current.userId,
88197
+ expiresAt: newExpiresAt
88198
+ });
88199
+ await tx.updateMany(
88200
+ FAMILIES,
88201
+ { familyId: { $eq: current.familyId } },
88202
+ { $set: { lastUsedAt: now } }
88203
+ );
88204
+ return {
88205
+ tag: "ok",
88206
+ result: { newJti, familyId: current.familyId, newRefreshExpiresAt: newExpiresAt }
88207
+ };
88208
+ });
88209
+ switch (outcome.tag) {
88210
+ case "ok":
88211
+ return outcome.result;
88212
+ case "not-found":
88213
+ throw new RefreshNotFoundError();
88214
+ case "expired":
88215
+ throw new RefreshExpiredError();
88216
+ case "reuse":
88217
+ await this.revokeFamily(outcome.familyId);
88218
+ throw new RefreshReuseDetectedError(outcome.familyId);
88219
+ }
88220
+ }
88221
+ async revokeFamily(familyId) {
88222
+ await this.ensureSchema();
88223
+ await this.docs.transaction(async (tx) => {
88224
+ await tx.deleteMany(REFRESHES, { familyId: { $eq: familyId } });
88225
+ await tx.deleteMany(FAMILIES, { familyId: { $eq: familyId } });
88226
+ });
88227
+ }
88228
+ async revokeAllUserSessions(userId) {
88229
+ await this.ensureSchema();
88230
+ await this.docs.transaction(async (tx) => {
88231
+ await tx.deleteMany(REFRESHES, { userId: { $eq: userId } });
88232
+ await tx.deleteMany(FAMILIES, { userId: { $eq: userId } });
88233
+ });
88234
+ }
88235
+ async revokeAllUserSessionsExcept(userId, exceptFamilyId) {
88236
+ await this.ensureSchema();
88237
+ await this.docs.transaction(async (tx) => {
88238
+ const families = await tx.find(FAMILIES, { userId: { $eq: userId } });
88239
+ const targets = families.filter((f) => f.familyId !== exceptFamilyId).map((f) => f.familyId);
88240
+ if (targets.length === 0) {
88241
+ return;
88242
+ }
88243
+ await tx.deleteMany(
88244
+ REFRESHES,
88245
+ { $and: [{ userId: { $eq: userId } }, { familyId: { $in: targets } }] }
88246
+ );
88247
+ await tx.deleteMany(
88248
+ FAMILIES,
88249
+ { $and: [{ userId: { $eq: userId } }, { familyId: { $in: targets } }] }
88250
+ );
88251
+ });
88252
+ }
88253
+ async listFamiliesByUser(userId) {
88254
+ await this.ensureSchema();
88255
+ const docs = await this.docs.find(
88256
+ FAMILIES,
88257
+ { userId: { $eq: userId } },
88258
+ { sort: { lastUsedAt: -1 } }
88259
+ );
88260
+ return docs.map((d) => ({
88261
+ familyId: d.familyId,
88262
+ userId: d.userId,
88263
+ tenantId: d.tenantId,
88264
+ createdAt: d.createdAt,
88265
+ lastUsedAt: d.lastUsedAt,
88266
+ userAgent: d.userAgent,
88267
+ ipFirst: d.ipFirst
88268
+ }));
88269
+ }
88270
+ };
88271
+ var noopLogger = {
88272
+ warn: () => void 0,
88273
+ info: () => void 0,
88274
+ error: () => void 0
88275
+ };
88276
+ var sha1Hex = (input) => crypto4.createHash("sha1").update(input).digest("hex").toUpperCase();
88277
+ var queryHibp = async (plain, doFetch) => {
88278
+ const hash2 = sha1Hex(plain);
88279
+ const prefix = hash2.slice(0, 5);
88280
+ const wantedSuffix = hash2.slice(5);
88281
+ const resp = await doFetch(`https://api.pwnedpasswords.com/range/${prefix}`);
88282
+ if (!resp.ok) {
88283
+ throw new Error(`HIBP ${resp.status}`);
88284
+ }
88285
+ const text = await resp.text();
88286
+ for (const line of text.split(/\r?\n/)) {
88287
+ const trimmed = line.trim();
88288
+ if (!trimmed) {
88289
+ continue;
88290
+ }
88291
+ const sepIdx = trimmed.indexOf(":");
88292
+ if (sepIdx < 0) {
88293
+ continue;
88294
+ }
88295
+ const suffix = trimmed.slice(0, sepIdx).toUpperCase();
88296
+ if (suffix === wantedSuffix) {
88297
+ const count = Number.parseInt(trimmed.slice(sepIdx + 1), 10);
88298
+ return Number.isFinite(count) ? count : 1;
88299
+ }
88300
+ }
88301
+ return 0;
88302
+ };
88303
+ var createPasswordPolicy = (opts) => {
88304
+ const log3 = opts.logger ?? noopLogger;
88305
+ const doFetch = opts.fetch ?? fetch;
88306
+ return {
88307
+ async validate(plain) {
88308
+ if (plain.length < opts.minLength) {
88309
+ return { ok: false, reason: "too_short" };
88310
+ }
88311
+ if (plain.length > opts.maxLength) {
88312
+ return { ok: false, reason: "too_long" };
88313
+ }
88314
+ if (!opts.hibpEnabled) {
88315
+ return { ok: true };
88316
+ }
88317
+ try {
88318
+ const count = await queryHibp(plain, doFetch);
88319
+ if (count > 0) {
88320
+ return { ok: false, reason: "pwned" };
88321
+ }
88322
+ return { ok: true };
88323
+ } catch (err) {
88324
+ log3.warn("password-policy: HIBP unavailable, accepting password", { err: String(err) });
88325
+ return { ok: true, warning: "hibp_unavailable" };
88326
+ }
88327
+ }
88328
+ };
88329
+ };
87787
88330
  external_exports.object({
87788
88331
  type: external_exports.literal("oidc"),
87789
88332
  id: external_exports.string().min(1),
@@ -87802,6 +88345,54 @@ external_exports.object({
87802
88345
  }).passthrough();
87803
88346
  Object.freeze(Object.values(PERMISSIONS));
87804
88347
  Object.freeze([PERMISSIONS.MACHINE_REGISTER]);
88348
+ var ResetAdminError = class extends Error {
88349
+ constructor(code, message2) {
88350
+ super(message2);
88351
+ this.code = code;
88352
+ this.name = "ResetAdminError";
88353
+ }
88354
+ code;
88355
+ };
88356
+ var resetAdmin = async (opts) => {
88357
+ const { users, credentials, memberships, sessions, tenantId, logger: logger2 } = opts;
88358
+ const email3 = canonicalizeEmail(opts.email);
88359
+ if (!email3 || !tenantId) {
88360
+ throw new ResetAdminError("invalid_input", "resetAdmin: email and tenantId are required");
88361
+ }
88362
+ const policy = await opts.passwordPolicy.validate(opts.password);
88363
+ if (!policy.ok) {
88364
+ throw new ResetAdminError("weak_password", `password rejected by policy: ${policy.reason}`);
88365
+ }
88366
+ const repairs = [];
88367
+ const existing = await users.findByEmailTenant(email3, tenantId);
88368
+ let userId;
88369
+ if (!existing) {
88370
+ userId = crypto4.randomUUID();
88371
+ await users.create({ userId, tenantId, email: email3, status: "active" });
88372
+ repairs.push("user-created");
88373
+ } else {
88374
+ userId = existing.userId;
88375
+ if (existing.status !== "active") {
88376
+ await users.setStatus(userId, "active");
88377
+ repairs.push("user-reactivated");
88378
+ }
88379
+ }
88380
+ const hash2 = await import_bcryptjs.default.hash(opts.password, opts.bcryptCost);
88381
+ await credentials.setCredential({ userId, providerId: "email-password", hash: hash2 });
88382
+ repairs.push("credential-set");
88383
+ const membership = (await memberships.listByUser(userId)).find((m) => m.tenantId === tenantId);
88384
+ if (!membership) {
88385
+ await memberships.addMembership({ userId, tenantId, groupId: "tenant-admin" });
88386
+ repairs.push("membership-created");
88387
+ } else if (membership.groupId !== "tenant-admin") {
88388
+ await memberships.setGroup(userId, tenantId, "tenant-admin");
88389
+ repairs.push("membership-promoted");
88390
+ }
88391
+ await sessions.revokeAllUserSessions(userId);
88392
+ const outcome = existing ? "reset" : "created";
88393
+ logger2.info("reset-admin: admin account restored", { userId, tenantId, outcome, repairs });
88394
+ return { userId, tenantId, email: email3, outcome, repairs };
88395
+ };
87805
88396
 
87806
88397
  // src/mcp/auth.ts
87807
88398
  var DEV_JWT_SECRET = "dev-insecure-secret-change-me";
@@ -110578,7 +111169,7 @@ function validateSegments(segs) {
110578
111169
  }
110579
111170
  return null;
110580
111171
  }
110581
- var noopLogger = {
111172
+ var noopLogger2 = {
110582
111173
  debug: () => {
110583
111174
  },
110584
111175
  info: () => {
@@ -110587,13 +111178,13 @@ var noopLogger = {
110587
111178
  },
110588
111179
  error: () => {
110589
111180
  },
110590
- child: () => noopLogger
111181
+ child: () => noopLogger2
110591
111182
  };
110592
111183
  var TrieBackedRegistry = class {
110593
111184
  systemRouter = new TrieRouter();
110594
111185
  pluginRouter = new TrieRouter();
110595
111186
  partial = false;
110596
- logger = noopLogger;
111187
+ logger = noopLogger2;
110597
111188
  // ─── Logger ────────────────────────────────────────────────────────────────
110598
111189
  setLogger(logger2) {
110599
111190
  this.logger = logger2;
@@ -110777,31 +111368,31 @@ defineSystemCommand({
110777
111368
  const cliCtx = ctx.hostContext?.host === "cli" ? ctx.hostContext : void 0;
110778
111369
  const cliVersion = cliCtx?.cliVersion ?? process.env.CLI_VERSION ?? "0.0.0";
110779
111370
  const nodeVersion = process.version;
110780
- const platform11 = `${process.platform} ${process.arch}`;
111371
+ const platform12 = `${process.platform} ${process.arch}`;
110781
111372
  ctx.platform?.logger?.info("Version command executed", {
110782
111373
  version: cliVersion,
110783
111374
  nodeVersion,
110784
- platform: platform11
111375
+ platform: platform12
110785
111376
  });
110786
111377
  if (!flags.json) {
110787
111378
  ctx.ui?.write(`KB Labs CLI v${cliVersion}
110788
111379
  `);
110789
111380
  ctx.ui?.write(`Node: ${nodeVersion}
110790
111381
  `);
110791
- ctx.ui?.write(`Platform: ${platform11}
111382
+ ctx.ui?.write(`Platform: ${platform12}
110792
111383
  `);
110793
111384
  } else {
110794
111385
  ctx.ui?.json({
110795
111386
  version: cliVersion,
110796
111387
  nodeVersion,
110797
- platform: platform11
111388
+ platform: platform12
110798
111389
  });
110799
111390
  }
110800
111391
  return {
110801
111392
  ok: true,
110802
111393
  version: cliVersion,
110803
111394
  nodeVersion,
110804
- platform: platform11
111395
+ platform: platform12
110805
111396
  };
110806
111397
  }
110807
111398
  });
@@ -113176,6 +113767,210 @@ defineSystemCommand({
113176
113767
  return { ok: true, clientId: data.clientId, clientSecret: data.clientSecret, hostId: data.hostId };
113177
113768
  }
113178
113769
  });
113770
+ var DEFAULT_TENANT = "kblabs-cloud";
113771
+ var DEFAULT_GATEWAY_HOST = "127.0.0.1";
113772
+ var DEFAULT_GATEWAY_PORT = 4e3;
113773
+ var PROBE_TIMEOUT_MS = 500;
113774
+ function isRecord3(v) {
113775
+ return typeof v === "object" && v !== null;
113776
+ }
113777
+ function gatewaySlice(config2) {
113778
+ const gw = config2?.gateway;
113779
+ return isRecord3(gw) ? gw : {};
113780
+ }
113781
+ async function readStdin() {
113782
+ const chunks = [];
113783
+ for await (const chunk of process.stdin) {
113784
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
113785
+ }
113786
+ return Buffer.concat(chunks).toString("utf-8").replace(/\r?\n$/, "");
113787
+ }
113788
+ function isPortListening(host, port) {
113789
+ return new Promise((resolve9) => {
113790
+ const socket = net.connect({ host, port });
113791
+ const done = (listening) => {
113792
+ socket.destroy();
113793
+ resolve9(listening);
113794
+ };
113795
+ socket.setTimeout(PROBE_TIMEOUT_MS, () => done(false));
113796
+ socket.once("connect", () => done(true));
113797
+ socket.once("error", () => done(false));
113798
+ });
113799
+ }
113800
+ async function planRepairs(existing, tenantId, memberships) {
113801
+ if (!existing) {
113802
+ return ["user-created", "credential-set", "membership-created"];
113803
+ }
113804
+ const repairs = [];
113805
+ if (existing.status !== "active") {
113806
+ repairs.push("user-reactivated");
113807
+ }
113808
+ repairs.push("credential-set");
113809
+ const membership = (await memberships.listByUser(existing.userId)).find((m) => m.tenantId === tenantId);
113810
+ if (!membership) {
113811
+ repairs.push("membership-created");
113812
+ } else if (membership.groupId !== "tenant-admin") {
113813
+ repairs.push("membership-promoted");
113814
+ }
113815
+ return repairs;
113816
+ }
113817
+ function printDryRun(ctx, existing, dry) {
113818
+ ctx.ui?.write?.(`Dry run \u2014 nothing changed.
113819
+ Admin: ${dry.email} (tenant ${dry.tenantId})
113820
+ `);
113821
+ ctx.ui?.write?.(existing ? ` State: exists, status=${existing.status}
113822
+ ` : " State: does not exist\n");
113823
+ ctx.ui?.write?.(` Would: ${dry.repairs?.join(", ")}, revoke all sessions
113824
+ `);
113825
+ ctx.ui?.write?.("Re-run with --yes and --password-stdin or --generate to apply.\n");
113826
+ }
113827
+ function printApplied(ctx, out) {
113828
+ ctx.ui?.write?.(`Admin ${out.outcome === "created" ? "created" : "restored"}: ${out.email} (tenant ${out.tenantId})
113829
+ `);
113830
+ ctx.ui?.write?.(` Changes: ${out.repairs?.join(", ")}; all sessions revoked
113831
+ `);
113832
+ if (out.generatedPassword) {
113833
+ ctx.ui?.write?.(` Password: ${out.generatedPassword}
113834
+ Save it now \u2014 it is not shown again.
113835
+ `);
113836
+ }
113837
+ }
113838
+ defineSystemCommand({
113839
+ name: "reset-admin",
113840
+ description: "Recover the tenant admin account (reset password, reactivate, restore role)",
113841
+ longDescription: "Offline recovery of the tenant admin against the platform database: creates the admin if missing, reactivates it, sets a new password, restores the tenant-admin membership and revokes all its sessions. Dry run unless --yes is given. The password is read from stdin (--password-stdin) or generated (--generate); it is never accepted as an argument.",
113842
+ category: "auth",
113843
+ examples: [
113844
+ "kb auth reset-admin",
113845
+ "kb auth reset-admin --generate --yes",
113846
+ 'printf %s "$NEW_PASSWORD" | kb auth reset-admin --password-stdin --yes',
113847
+ "kb auth reset-admin --email admin@example.com --tenant acme --generate --yes --json"
113848
+ ],
113849
+ flags: {
113850
+ email: { type: "string", description: "Admin email (default: GATEWAY_BOOTSTRAP_ADMIN_EMAIL or gateway.auth.bootstrap.adminEmail)" },
113851
+ tenant: { type: "string", description: `Tenant id (default: gateway.auth.bootstrap.tenantId, then GATEWAY_BOOTSTRAP_TENANT_ID, else ${DEFAULT_TENANT} \u2014 the gateway's own order)` },
113852
+ "password-stdin": { type: "boolean", description: "Read the new password from stdin" },
113853
+ generate: { type: "boolean", description: "Generate a random password and print it once" },
113854
+ yes: { type: "boolean", description: "Apply the changes (without it this is a dry run)" },
113855
+ force: { type: "boolean", description: "Run even if a gateway is listening on the configured port" },
113856
+ json: { type: "boolean", description: "Output in JSON format" }
113857
+ },
113858
+ async handler(ctx, _argv, flags) {
113859
+ const fail2 = (msg) => {
113860
+ if (flags.json) {
113861
+ ctx.ui?.json({ ok: false, error: msg });
113862
+ } else {
113863
+ ctx.ui?.error?.(msg);
113864
+ }
113865
+ return { ok: false, error: msg };
113866
+ };
113867
+ if (flags["password-stdin"] && flags.generate) {
113868
+ return fail2("Use either --password-stdin or --generate, not both.");
113869
+ }
113870
+ if (flags.yes && !flags["password-stdin"] && !flags.generate) {
113871
+ return fail2("A password source is required to apply: pass --password-stdin or --generate.");
113872
+ }
113873
+ const loaded = await loadPlatformConfig({ startDir: getContextCwd(ctx), loadEnvFile: true });
113874
+ const gw = gatewaySlice(loaded.effectiveConfig ?? loaded.rawConfig ?? loaded.rawPlatformConfig);
113875
+ const email3 = canonicalizeEmail(
113876
+ flags.email ?? process.env.GATEWAY_BOOTSTRAP_ADMIN_EMAIL ?? gw.auth?.bootstrap?.adminEmail ?? ""
113877
+ );
113878
+ if (!email3) {
113879
+ return fail2("Admin email is unknown: pass --email, or set GATEWAY_BOOTSTRAP_ADMIN_EMAIL / gateway.auth.bootstrap.adminEmail.");
113880
+ }
113881
+ const tenantId = flags.tenant ?? gw.auth?.bootstrap?.tenantId ?? process.env.GATEWAY_BOOTSTRAP_TENANT_ID ?? DEFAULT_TENANT;
113882
+ if (!platform.isReal("documentDatabase")) {
113883
+ return fail2(
113884
+ "No persistent documentDatabase is configured for this project, so there is no admin account on disk to recover. Check `adapters.documentDatabase` in .kb/kb.config.json."
113885
+ );
113886
+ }
113887
+ if (!flags.force) {
113888
+ const host = gw.host ?? DEFAULT_GATEWAY_HOST;
113889
+ const port = gw.port ?? DEFAULT_GATEWAY_PORT;
113890
+ if (await isPortListening(host, port)) {
113891
+ return fail2(
113892
+ `A gateway appears to be running on ${host}:${port}. Stop it first (kb-dev stop gateway) so the database is not written concurrently, or pass --force if that is not the gateway.`
113893
+ );
113894
+ }
113895
+ }
113896
+ const docs = platform.documentDatabase;
113897
+ const users = new UsersStore(docs);
113898
+ const credentials = new CredentialsStore(docs);
113899
+ const memberships = new MembershipsStore(docs);
113900
+ const sessions = new SessionsStore(docs, {
113901
+ refreshTtlMs: 0,
113902
+ graceWindowMs: 0
113903
+ });
113904
+ const existing = await users.findByEmailTenant(email3, tenantId);
113905
+ if (!flags.yes) {
113906
+ const repairs = await planRepairs(existing, tenantId, memberships);
113907
+ const dry = {
113908
+ ok: true,
113909
+ dryRun: true,
113910
+ email: email3,
113911
+ tenantId,
113912
+ userId: existing?.userId,
113913
+ outcome: existing ? "reset" : "created",
113914
+ repairs
113915
+ };
113916
+ if (flags.json) {
113917
+ ctx.ui?.json(dry);
113918
+ } else {
113919
+ printDryRun(ctx, existing, dry);
113920
+ }
113921
+ return dry;
113922
+ }
113923
+ const generated = flags.generate ? crypto4.randomBytes(18).toString("base64url") : void 0;
113924
+ const password = generated ?? await readStdin();
113925
+ if (!password) {
113926
+ return fail2("Empty password on stdin.");
113927
+ }
113928
+ const policy = gw.auth?.passwordPolicy;
113929
+ const bcryptCost = gw.auth?.bcryptCost ?? 12;
113930
+ try {
113931
+ const result = await resetAdmin({
113932
+ users,
113933
+ credentials,
113934
+ memberships,
113935
+ sessions,
113936
+ passwordPolicy: createPasswordPolicy({
113937
+ minLength: policy?.minLength ?? 8,
113938
+ maxLength: policy?.maxLength ?? 256,
113939
+ hibpEnabled: policy?.hibpEnabled ?? true
113940
+ }),
113941
+ bcryptCost,
113942
+ tenantId,
113943
+ email: email3,
113944
+ password,
113945
+ logger: {
113946
+ info: (...args) => platform.logger.info(String(args[0]), isRecord3(args[1]) ? args[1] : void 0),
113947
+ warn: (...args) => platform.logger.warn(String(args[0]), isRecord3(args[1]) ? args[1] : void 0)
113948
+ }
113949
+ });
113950
+ const out = {
113951
+ ok: true,
113952
+ dryRun: false,
113953
+ email: result.email,
113954
+ tenantId: result.tenantId,
113955
+ userId: result.userId,
113956
+ outcome: result.outcome,
113957
+ repairs: result.repairs,
113958
+ ...generated ? { generatedPassword: generated } : {}
113959
+ };
113960
+ if (flags.json) {
113961
+ ctx.ui?.json(out);
113962
+ } else {
113963
+ printApplied(ctx, out);
113964
+ }
113965
+ return out;
113966
+ } catch (err) {
113967
+ if (err instanceof ResetAdminError) {
113968
+ return fail2(err.message);
113969
+ }
113970
+ throw err;
113971
+ }
113972
+ }
113973
+ });
113179
113974
  function isMode(value) {
113180
113975
  return value === "validate" || value === "reconcile" || value === "auto";
113181
113976
  }