@amerged/ohmyhost-cli 0.1.18 → 0.1.20

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.
@@ -1,11 +1,14 @@
1
1
  import { createRequire as __ohmyhostCreateRequire } from "node:module"; const require = __ohmyhostCreateRequire(import.meta.url);
2
2
  import {
3
+ CredentialStoreIntegrityError,
3
4
  CredentialStoreUnavailableError,
5
+ LEGACY_CREDENTIAL_ACCOUNT,
4
6
  NativeNapiKeyringCredentialStore,
5
7
  __commonJS,
6
8
  __require,
7
- __toESM
8
- } from "./chunk-4HFQ54FP.js";
9
+ __toESM,
10
+ createNativeEntryFactory
11
+ } from "./chunk-7D2RFLWM.js";
9
12
 
10
13
  // node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
11
14
  var require_code = __commonJS({
@@ -14930,7 +14933,398 @@ async function requireIgnoredTokenFile(path) {
14930
14933
  }
14931
14934
  }
14932
14935
 
14936
+ // apps/product-cli/dist/profile-store.js
14937
+ import { randomUUID } from "node:crypto";
14938
+ import { link, mkdir as mkdir2, open as open2, readFile, readdir, unlink } from "node:fs/promises";
14939
+ import { homedir } from "node:os";
14940
+ import { join } from "node:path";
14941
+ var ProfileNameConflictError = class extends Error {
14942
+ profile;
14943
+ constructor(profile) {
14944
+ super("The profile name is used by another saved login");
14945
+ this.profile = profile;
14946
+ this.name = "ProfileNameConflictError";
14947
+ }
14948
+ };
14949
+ var ProfileLimitError = class extends Error {
14950
+ saved;
14951
+ constructor(saved) {
14952
+ super("The saved logins of this environment reached their limit");
14953
+ this.saved = saved;
14954
+ this.name = "ProfileLimitError";
14955
+ }
14956
+ };
14957
+ var MAX_PROFILES = 64;
14958
+ var MAX_INDEX_ATTEMPTS = 64;
14959
+ var INDEX_FILE = /^index-([1-9][0-9]{0,14})\.json$/u;
14960
+ var SERVICE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
14961
+ var PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/u;
14962
+ var USER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
14963
+ var ORGANIZATION_ID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
14964
+ var isProfileName = (value) => PROFILE_NAME.test(value);
14965
+ var isProfileUserId = (value) => USER_ID.test(value);
14966
+ var profileAccount = (profile) => `profile:${profile.userId}:${profile.organizationId ?? "none"}`;
14967
+ var sameLogin = (left, right) => profileAccount(left) === profileAccount(right);
14968
+ var profileIndexDirectory = (serviceName) => join(homedir(), ".ohmyhost", "profiles", serviceName);
14969
+ var nextProfileIndex = (current, profile, replaces) => [
14970
+ ...current.filter((saved) => !sameLogin(saved, profile) && (replaces === void 0 || !sameLogin(saved, replaces))),
14971
+ profile
14972
+ ];
14973
+ var admitProfile = (next, profile) => {
14974
+ const conflict = next.find((saved) => saved.name === profile.name && saved !== profile);
14975
+ if (conflict !== void 0)
14976
+ throw new ProfileNameConflictError(conflict);
14977
+ if (next.length > MAX_PROFILES)
14978
+ throw new ProfileLimitError(next.length - 1);
14979
+ };
14980
+ var NativeKeyringProfileRegistry = class {
14981
+ serviceName;
14982
+ #createEntry;
14983
+ #index;
14984
+ constructor(createEntry = void 0, serviceName = "ohmyhost", indexDirectory) {
14985
+ this.serviceName = serviceName;
14986
+ if (!SERVICE_NAME.test(serviceName))
14987
+ throw new CredentialStoreUnavailableError();
14988
+ this.#createEntry = createEntry ?? createNativeEntryFactory(serviceName);
14989
+ this.#index = new ProfileIndexFiles(indexDirectory ?? profileIndexDirectory(serviceName));
14990
+ }
14991
+ async list() {
14992
+ return (await this.#index.read()).profiles;
14993
+ }
14994
+ credential(profile) {
14995
+ return new NativeNapiKeyringCredentialStore(this.#createEntry, this.serviceName, profileAccount(profile));
14996
+ }
14997
+ legacy() {
14998
+ return new NativeNapiKeyringCredentialStore(this.#createEntry, this.serviceName, LEGACY_CREDENTIAL_ACCOUNT);
14999
+ }
15000
+ async save(profile, credential, replaces) {
15001
+ assertProfile(profile);
15002
+ const before = await this.list();
15003
+ admitProfile(nextProfileIndex(before, profile, replaces), profile);
15004
+ await this.credential(profile).replace(credential);
15005
+ try {
15006
+ await this.#index.update((current) => {
15007
+ const next = nextProfileIndex(current, profile, replaces);
15008
+ admitProfile(next, profile);
15009
+ return next;
15010
+ });
15011
+ } catch (error) {
15012
+ if (!before.some((saved) => sameLogin(saved, profile)))
15013
+ await this.credential(profile).clear();
15014
+ throw error;
15015
+ }
15016
+ if (replaces !== void 0 && !sameLogin(replaces, profile))
15017
+ await this.credential(replaces).clear();
15018
+ }
15019
+ async remove(profile) {
15020
+ await this.credential(profile).clear();
15021
+ await this.#index.update((current) => current.some((saved) => sameLogin(saved, profile)) ? current.filter((saved) => !sameLogin(saved, profile)) : void 0);
15022
+ }
15023
+ };
15024
+ var ProfileIndexFiles = class {
15025
+ directory;
15026
+ constructor(directory) {
15027
+ this.directory = directory;
15028
+ }
15029
+ async read() {
15030
+ for (let attempt = 0; attempt < MAX_INDEX_ATTEMPTS; attempt += 1) {
15031
+ const version2 = await this.#newest();
15032
+ if (version2 === 0)
15033
+ return { version: version2, profiles: [] };
15034
+ let serialized;
15035
+ try {
15036
+ serialized = await readFile(this.#file(version2), "utf8");
15037
+ } catch (error) {
15038
+ if (fileErrorCode(error) === "ENOENT")
15039
+ continue;
15040
+ throw new CredentialStoreUnavailableError();
15041
+ }
15042
+ return { version: version2, profiles: parseProfileIndex(serialized) };
15043
+ }
15044
+ throw new CredentialStoreUnavailableError();
15045
+ }
15046
+ /** Applies `change` to the newest index; `undefined` means nothing to change. */
15047
+ async update(change) {
15048
+ try {
15049
+ await mkdir2(this.directory, { recursive: true, mode: 448 });
15050
+ } catch {
15051
+ throw new CredentialStoreUnavailableError();
15052
+ }
15053
+ for (let attempt = 0; attempt < MAX_INDEX_ATTEMPTS; attempt += 1) {
15054
+ const { version: version2, profiles } = await this.read();
15055
+ const next = change(profiles);
15056
+ if (next === void 0)
15057
+ return;
15058
+ const temporary = join(this.directory, `.index-${randomUUID()}.tmp`);
15059
+ try {
15060
+ await writeDurably(temporary, serializeProfileIndex(next));
15061
+ await link(temporary, this.#file(version2 + 1));
15062
+ } catch (error) {
15063
+ if (fileErrorCode(error) === "EEXIST")
15064
+ continue;
15065
+ throw new CredentialStoreUnavailableError();
15066
+ } finally {
15067
+ await removeTemporary(temporary);
15068
+ }
15069
+ if (await this.#newest() !== version2 + 1)
15070
+ continue;
15071
+ await this.#prune(version2);
15072
+ return;
15073
+ }
15074
+ throw new CredentialStoreUnavailableError();
15075
+ }
15076
+ async #newest() {
15077
+ let names;
15078
+ try {
15079
+ names = await readdir(this.directory);
15080
+ } catch (error) {
15081
+ if (fileErrorCode(error) === "ENOENT")
15082
+ return 0;
15083
+ throw new CredentialStoreUnavailableError();
15084
+ }
15085
+ return names.reduce((newest, name) => Math.max(newest, indexVersion(name)), 0);
15086
+ }
15087
+ /**
15088
+ * Keeps the superseded version for readers that listed it just now; older ones go. Cleanup only:
15089
+ * a file left behind is ignored, since readers take the newest version, and another process may
15090
+ * be removing the same files at this moment.
15091
+ */
15092
+ async #prune(superseded) {
15093
+ let names;
15094
+ try {
15095
+ names = await readdir(this.directory);
15096
+ } catch {
15097
+ return;
15098
+ }
15099
+ for (const name of names) {
15100
+ const version2 = indexVersion(name);
15101
+ if (version2 === 0 || version2 >= superseded)
15102
+ continue;
15103
+ try {
15104
+ await unlink(join(this.directory, name));
15105
+ } catch {
15106
+ }
15107
+ }
15108
+ }
15109
+ #file(version2) {
15110
+ return join(this.directory, `index-${String(version2)}.json`);
15111
+ }
15112
+ };
15113
+ var indexVersion = (name) => Number(INDEX_FILE.exec(name)?.[1] ?? 0);
15114
+ var removeTemporary = async (path) => {
15115
+ try {
15116
+ await unlink(path);
15117
+ } catch {
15118
+ }
15119
+ };
15120
+ var fileErrorCode = (error) => typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
15121
+ var writeDurably = async (path, text) => {
15122
+ const handle = await open2(path, "wx", 384);
15123
+ try {
15124
+ await handle.writeFile(text, "utf8");
15125
+ await handle.sync();
15126
+ } finally {
15127
+ await handle.close();
15128
+ }
15129
+ };
15130
+ var serializeProfileIndex = (profiles) => JSON.stringify({
15131
+ version: 1,
15132
+ profiles: [...profiles].sort((left, right) => left.name < right.name ? -1 : 1).map((profile) => ({
15133
+ name: profile.name,
15134
+ user_id: profile.userId,
15135
+ organization_id: profile.organizationId,
15136
+ organization_name: profile.organizationName
15137
+ }))
15138
+ });
15139
+ var parseProfileIndex = (serialized) => {
15140
+ try {
15141
+ const value = JSON.parse(serialized);
15142
+ if (!isRecord(value, ["version", "profiles"]) || value["version"] !== 1)
15143
+ throw new Error("Invalid profile index");
15144
+ const entries = value["profiles"];
15145
+ if (!Array.isArray(entries) || entries.length > MAX_PROFILES)
15146
+ throw new Error("Invalid profile index");
15147
+ const profiles = entries.map((entry) => {
15148
+ if (!isRecord(entry, ["name", "user_id", "organization_id", "organization_name"]))
15149
+ throw new Error("Invalid profile");
15150
+ const profile = {
15151
+ name: entry["name"],
15152
+ userId: entry["user_id"],
15153
+ organizationId: entry["organization_id"],
15154
+ organizationName: entry["organization_name"]
15155
+ };
15156
+ assertProfile(profile);
15157
+ return profile;
15158
+ });
15159
+ if (new Set(profiles.map((profile) => profile.name)).size !== profiles.length || new Set(profiles.map(profileAccount)).size !== profiles.length)
15160
+ throw new Error("Duplicate profile");
15161
+ return profiles;
15162
+ } catch {
15163
+ throw new CredentialStoreIntegrityError();
15164
+ }
15165
+ };
15166
+ var assertProfile = (profile) => {
15167
+ if (typeof profile.name !== "string" || !PROFILE_NAME.test(profile.name) || typeof profile.userId !== "string" || !USER_ID.test(profile.userId) || !(profile.organizationId === null || typeof profile.organizationId === "string" && ORGANIZATION_ID.test(profile.organizationId)) || !(profile.organizationName === null || typeof profile.organizationName === "string" && profile.organizationName.length > 0 && profile.organizationName.length <= 128 && !new RegExp("\\p{Cc}", "u").test(profile.organizationName)) || profile.organizationId === null && profile.organizationName !== null)
15168
+ throw new CredentialStoreIntegrityError();
15169
+ };
15170
+ var isRecord = (value, keys) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).sort().join(",") === [...keys].sort().join(",");
15171
+ var selectProfile = (profiles, request) => {
15172
+ if (request.name !== void 0 && request.bound !== void 0 && request.name !== request.bound)
15173
+ return { outcome: "conflict", field: "binding", profile: null };
15174
+ const named = request.name ?? request.bound;
15175
+ if (named !== void 0) {
15176
+ const profile = profiles.find((saved) => saved.name === named);
15177
+ if (profile === void 0)
15178
+ return { outcome: "not_found" };
15179
+ if (request.organizationId !== void 0 && profile.organizationId !== request.organizationId)
15180
+ return { outcome: "conflict", field: "organization", profile };
15181
+ if (request.userId !== void 0 && profile.userId !== request.userId)
15182
+ return { outcome: "conflict", field: "user", profile };
15183
+ return { outcome: "selected", profile };
15184
+ }
15185
+ const candidates = profiles.filter((profile) => (request.organizationId === void 0 || profile.organizationId === request.organizationId) && (request.userId === void 0 || profile.userId === request.userId));
15186
+ const only = candidates[0];
15187
+ if (candidates.length === 1 && only !== void 0)
15188
+ return { outcome: "selected", profile: only };
15189
+ if (candidates.length > 1)
15190
+ return { outcome: "ambiguous", candidates };
15191
+ return profiles.length === 0 ? { outcome: "none" } : { outcome: "not_found" };
15192
+ };
15193
+ var selectProfileForOrganization = (profiles, request) => {
15194
+ const user = request.userId === void 0 ? {} : { userId: request.userId };
15195
+ if (request.name !== void 0 || request.bound !== void 0)
15196
+ return selectProfile(profiles, {
15197
+ ...request.name === void 0 ? {} : { name: request.name },
15198
+ ...request.bound === void 0 ? {} : { bound: request.bound },
15199
+ ...user
15200
+ });
15201
+ const candidates = profiles.filter((profile) => request.userId === void 0 || profile.userId === request.userId);
15202
+ const inOrganization = candidates.filter((profile) => profile.organizationId === request.organizationId);
15203
+ if (inOrganization.length > 1)
15204
+ return { outcome: "ambiguous", candidates: inOrganization };
15205
+ const unscoped = candidates.filter((profile) => profile.organizationId === null);
15206
+ const [member] = inOrganization;
15207
+ if (member !== void 0)
15208
+ return { outcome: "selected", profile: member };
15209
+ const [open5] = unscoped;
15210
+ if (unscoped.length === 1 && open5 !== void 0)
15211
+ return { outcome: "selected", profile: open5 };
15212
+ return selectProfile(profiles, user);
15213
+ };
15214
+ var defaultProfileName = (profiles, login) => {
15215
+ const same = profiles.find((profile) => profile.userId === login.userId && profile.organizationId === login.organizationId);
15216
+ if (same !== void 0)
15217
+ return same.name;
15218
+ const base = slug(login.organizationName ?? "") || (login.organizationId === null ? "account" : "workspace");
15219
+ const free = (name) => profiles.every((profile) => profile.name !== name);
15220
+ if (free(base))
15221
+ return base;
15222
+ const suffix = (login.organizationId ?? login.userId).replace(/[^A-Za-z0-9]/gu, "").slice(-6).toLowerCase();
15223
+ for (let attempt = 1; attempt <= profiles.length + 1; attempt += 1) {
15224
+ const candidate = `${base.slice(0, 48)}-${suffix}${attempt === 1 ? "" : `-${String(attempt)}`}`;
15225
+ if (free(candidate))
15226
+ return candidate;
15227
+ }
15228
+ throw new CredentialStoreIntegrityError();
15229
+ };
15230
+ var slug = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+/u, "").slice(0, 40).replace(/-+$/u, "");
15231
+ var publicProfile = (profile) => ({
15232
+ name: profile.name,
15233
+ user_id: profile.userId,
15234
+ organization_id: profile.organizationId,
15235
+ organization_name: profile.organizationName
15236
+ });
15237
+ var profileLimitAction = (commandPrefix) => `Nothing was saved: this environment already keeps ${String(MAX_PROFILES)} saved logins, the most one computer holds. Remove one you no longer need with '${commandPrefix} logout --profile-name NAME --json' ('${commandPrefix} profile list --json' shows them), then retry.`;
15238
+ var ProfileSelectionError = class extends Error {
15239
+ code;
15240
+ exitCode;
15241
+ suggestedAction;
15242
+ constructor(code, exitCode, suggestedAction) {
15243
+ super(`${code}: ${suggestedAction}`);
15244
+ this.code = code;
15245
+ this.exitCode = exitCode;
15246
+ this.suggestedAction = suggestedAction;
15247
+ this.name = "ProfileSelectionError";
15248
+ }
15249
+ };
15250
+ var profileSelectionError = (selection, request, commandPrefix, selector) => {
15251
+ const choose = selector === "cli" ? `pass --profile-name NAME (see '${commandPrefix} profile list --json') or set OHMYHOST_PROFILE for this process` : "pass profile_name (see profile_list) or set OHMYHOST_PROFILE in this MCP server's configuration";
15252
+ const login = `${commandPrefix} login${request.organizationId === void 0 ? "" : ` --organization ${request.organizationId}`}${request.userId === void 0 ? "" : ` --user ${request.userId}`}${(request.name ?? request.bound) === void 0 ? "" : ` --profile-name ${String(request.name ?? request.bound)}`} --json`;
15253
+ if (selection.outcome === "none")
15254
+ return new ProfileSelectionError("authentication_required", 5, `No login is saved on this computer. Run '${login}'.`);
15255
+ if (selection.outcome === "not_found")
15256
+ return new ProfileSelectionError("profile_not_found", 5, `No saved login matches ${describeRequest(request)}. Run '${login}' to add it, signing in as that user; '${commandPrefix} profile list --json' shows the saved logins.`);
15257
+ if (selection.outcome === "ambiguous")
15258
+ return new ProfileSelectionError("profile_selection_required", 2, `Several saved logins could run this: ${selection.candidates.map((profile2) => profile2.name).join(", ")}. Ask the customer which account to use, then ${choose}.`);
15259
+ if (selection.field === "binding")
15260
+ return new ProfileSelectionError("profile_context_mismatch", 2, `This call names profile ${String(request.name)}, but the process is bound to OHMYHOST_PROFILE=${String(request.bound)}. Use the bound profile or start a process for the other one.`);
15261
+ const profile = selection.profile;
15262
+ return new ProfileSelectionError("profile_context_mismatch", 2, selection.field === "organization" ? `Profile ${String(profile?.name)} is signed in to organization ${String(profile?.organizationId ?? "none")}, but this targets organization ${String(request.organizationId)}. Use the profile of that organization or change the target; nothing was sent.` : `Profile ${String(profile?.name)} belongs to user ${String(profile?.userId)}, not ${String(request.userId)}. Use that user's profile; nothing was sent.`);
15263
+ };
15264
+ var describeRequest = (request) => {
15265
+ const parts = [
15266
+ ...(request.name ?? request.bound) === void 0 ? [] : [`profile ${String(request.name ?? request.bound)}`],
15267
+ ...request.organizationId === void 0 ? [] : [`organization ${request.organizationId}`],
15268
+ ...request.userId === void 0 ? [] : [`user ${request.userId}`]
15269
+ ];
15270
+ return parts.length === 0 ? "this request" : parts.join(", ");
15271
+ };
15272
+ var describeProfile = (profile) => `profile ${profile.name} (user ${profile.userId}, ${profile.organizationId === null ? "no organization" : `organization ${profile.organizationName === null ? "" : `${profile.organizationName} `}${profile.organizationId}`})`;
15273
+
15274
+ // packages/workos-auth-contracts/dist/parsing.js
15275
+ var WorkosContractError = class extends Error {
15276
+ code;
15277
+ constructor(code, message) {
15278
+ super(message);
15279
+ this.code = code;
15280
+ this.name = "WorkosContractError";
15281
+ }
15282
+ };
15283
+ var expectRecord = (value, code = "invalid_object") => {
15284
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
15285
+ throw new WorkosContractError(code, "WorkOS response must be an object");
15286
+ }
15287
+ return value;
15288
+ };
15289
+ function expectString(value, field, options = {}) {
15290
+ if (value === void 0 && options.optional === true)
15291
+ return void 0;
15292
+ if (typeof value !== "string" || value.length === 0) {
15293
+ throw new WorkosContractError("invalid_field", `WorkOS field ${field} must be a non-empty string`);
15294
+ }
15295
+ return value;
15296
+ }
15297
+ function expectPositiveInteger(value, field, options = {}) {
15298
+ if (value === void 0 && options.optional === true)
15299
+ return void 0;
15300
+ if (!Number.isSafeInteger(value) || value <= 0) {
15301
+ throw new WorkosContractError("invalid_field", `WorkOS field ${field} must be positive`);
15302
+ }
15303
+ return value;
15304
+ }
15305
+ var parseJsonObject = (body) => {
15306
+ try {
15307
+ return expectRecord(JSON.parse(body));
15308
+ } catch (error) {
15309
+ if (error instanceof WorkosContractError)
15310
+ throw error;
15311
+ throw new WorkosContractError("invalid_json", "WorkOS returned invalid JSON");
15312
+ }
15313
+ };
15314
+
15315
+ // packages/workos-auth-contracts/dist/user-api-keys.js
15316
+ var WORKOS_USER_API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{20,128}$/u;
15317
+
14933
15318
  // apps/product-cli/dist/command.js
15319
+ var extractProfileName = (argv) => {
15320
+ const index = argv.indexOf("--profile-name");
15321
+ if (index < 0)
15322
+ return { argv };
15323
+ const value = argv[index + 1];
15324
+ if (value === void 0 || !isProfileName(value) || argv.indexOf("--profile-name", index + 1) >= 0)
15325
+ throw new InvalidCommandError();
15326
+ return { argv: [...argv.slice(0, index), ...argv.slice(index + 2)], profileName: value };
15327
+ };
14934
15328
  var InvalidCommandError = class extends Error {
14935
15329
  constructor() {
14936
15330
  super("The command arguments are invalid.");
@@ -14945,11 +15339,17 @@ var recognizedCommandLabel = (argv) => {
14945
15339
  return "version";
14946
15340
  while (values.length > 0) {
14947
15341
  const value = values.shift();
15342
+ if (value === "--profile-name") {
15343
+ values.shift();
15344
+ continue;
15345
+ }
14948
15346
  if (value === "--json" || value === "--dry-run" || value === "--revoke" || value === "--yes" || value === "--wait" || value === "--follow" || value === "--stdin") {
14949
15347
  continue;
14950
15348
  }
14951
15349
  if (value === "help" || value === "version" || value === "whoami" || value === "init" || value === "login" || value === "logout")
14952
15350
  return value;
15351
+ if (value === "profile" && values[0] === "list")
15352
+ return "profile list";
14953
15353
  if (value === "github" && values[0] === "connect")
14954
15354
  return "github connect";
14955
15355
  if (value === "github" && values[0] === "status")
@@ -15159,16 +15559,20 @@ var parseProductCliCommand = (argv) => {
15159
15559
  if (dryRun)
15160
15560
  throw new InvalidCommandError();
15161
15561
  if (first === "login" && !revoke && !yes && !wait && !follow && !stdin) {
15162
- const options = parseOptionalOptions(values, ["--organization"]);
15562
+ const options = parseOptionalOptions(values, ["--organization", "--user"]);
15163
15563
  const organizationId = options["--organization"];
15164
- if (organizationId !== void 0 && !ORGANIZATION_ULID.test(organizationId))
15564
+ const userId = options["--user"];
15565
+ if (organizationId !== void 0 && !ORGANIZATION_ULID.test(organizationId) || userId !== void 0 && !isProfileUserId(userId))
15165
15566
  throw new InvalidCommandError();
15166
15567
  return {
15167
15568
  kind: "login",
15168
15569
  ...organizationId === void 0 ? {} : { organizationId },
15570
+ ...userId === void 0 ? {} : { userId },
15169
15571
  credentialStore
15170
15572
  };
15171
15573
  }
15574
+ if (first === "profile" && values.length === 1 && values[0] === "list" && !revoke && !yes && !wait && !follow && !stdin)
15575
+ return { kind: "profile-list" };
15172
15576
  if (first === "logout") {
15173
15577
  if (!yes && !wait && !follow && !stdin && values.length === 0)
15174
15578
  return { kind: "logout", revoke, credentialStore };
@@ -16092,15 +16496,32 @@ var parseProductCliCommand = (argv) => {
16092
16496
  const name = values.shift();
16093
16497
  if (!isUserSecretName(name))
16094
16498
  throw new InvalidCommandError();
16095
- const options = parseOptions(values, ["--project", "--environment", "--idempotency-key"]);
16499
+ const options = parseOptionalOptions(values, [
16500
+ "--project",
16501
+ "--environment",
16502
+ "--idempotency-key",
16503
+ "--token-user",
16504
+ "--token-organization",
16505
+ "--profile-user",
16506
+ "--profile-organization"
16507
+ ]);
16508
+ const projectId = options["--project"];
16509
+ const environmentId = options["--environment"];
16510
+ const idempotencyKey = options["--idempotency-key"];
16511
+ const environmentToken2 = handedAccount(options["--token-user"], options["--token-organization"]);
16512
+ const profileAccount2 = handedAccount(options["--profile-user"], options["--profile-organization"]);
16513
+ if (projectId === void 0 || environmentId === void 0 || idempotencyKey === void 0 || environmentToken2 !== void 0 && profileAccount2 !== void 0)
16514
+ throw new InvalidCommandError();
16096
16515
  return {
16097
16516
  kind: "secret-set",
16098
- projectId: options["--project"],
16099
- environmentId: options["--environment"],
16517
+ projectId,
16518
+ environmentId,
16100
16519
  name,
16101
- idempotencyKey: options["--idempotency-key"],
16520
+ idempotencyKey,
16102
16521
  stdin,
16103
16522
  wait,
16523
+ ...environmentToken2 === void 0 ? {} : { environmentToken: environmentToken2 },
16524
+ ...profileAccount2 === void 0 ? {} : { profileAccount: profileAccount2 },
16104
16525
  credentialStore
16105
16526
  };
16106
16527
  }
@@ -16349,6 +16770,13 @@ var accessTtlSeconds = (value) => {
16349
16770
  return seconds;
16350
16771
  };
16351
16772
  var ORGANIZATION_ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
16773
+ var handedAccount = (userId, organizationId) => {
16774
+ if (userId === void 0 && organizationId === void 0)
16775
+ return void 0;
16776
+ if (userId === void 0 || organizationId === void 0 || !isProfileUserId(userId) || WORKOS_USER_API_KEY_PATTERN.test(userId) || !ORGANIZATION_ULID.test(organizationId))
16777
+ throw new InvalidCommandError();
16778
+ return { userId, organizationId };
16779
+ };
16352
16780
  var isValue = (value) => value !== void 0 && value.length > 0 && !value.startsWith("--");
16353
16781
  var validGithubBranchName = (value) => !(value.length < 1 || value.length > 1024 || !/^[A-Za-z0-9]/u.test(value) || value.startsWith("-") || value.startsWith("/") || value.endsWith("/") || value.endsWith(".") || value.includes("//") || value.includes("..") || value.includes("@{") || [...value].some((character) => {
16354
16782
  const code = character.codePointAt(0) ?? 0;
@@ -16357,46 +16785,6 @@ var validGithubBranchName = (value) => !(value.length < 1 || value.length > 1024
16357
16785
  var validPaidHostname = (value) => /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(value) && !value.endsWith(".poc.aixyte.com") && value !== "omh.st" && !value.endsWith(".omh.st") && value !== "ohmyho.st" && !value.endsWith(".ohmyho.st");
16358
16786
  var isUserSecretName = (value) => isValue(value) && /^[A-Z][A-Z0-9_]{0,127}$/u.test(value) && value !== "DATABASE_URL" && value !== "HYPERDRIVE" && value !== "BETTER_AUTH_SECRET" && value !== "BETTER_AUTH_URL" && value !== "OHMYHOST_MAIL_KEY" && value !== "OHMYHOST_MAIL_GATEWAY_URL";
16359
16787
 
16360
- // apps/product-cli/dist/workspace.js
16361
- var InteractiveSessionRequiredError = class extends Error {
16362
- constructor() {
16363
- super("Workspace selection requires an interactive login");
16364
- this.name = "InteractiveSessionRequiredError";
16365
- }
16366
- };
16367
- var chooseWorkspace = (organizations, requested) => {
16368
- if (requested !== void 0) {
16369
- const match = organizations.find((organization) => organization.id === requested);
16370
- return match === void 0 ? { outcome: "not_a_member", organizations } : { outcome: "selected", organization: match };
16371
- }
16372
- if (organizations.length === 0)
16373
- return { outcome: "no_workspace" };
16374
- const only = organizations[0];
16375
- if (organizations.length === 1 && only !== void 0)
16376
- return { outcome: "selected", organization: only };
16377
- return { outcome: "choice_required", organizations };
16378
- };
16379
- var selectWorkspace = async (session, organization, signal) => {
16380
- const stored = await session.store.read();
16381
- if (stored === void 0)
16382
- throw new InteractiveSessionRequiredError();
16383
- const refreshed = await session.refresher.refresh(stored.refreshToken, signal, organization.workos_id);
16384
- await session.store.replace({
16385
- ...refreshed,
16386
- workosOrganizationId: organization.workos_id,
16387
- organizationId: organization.id
16388
- });
16389
- };
16390
- var selectedWorkspaceId = async (session) => (await session.store.read())?.organizationId ?? null;
16391
- var workspaceOf = (credential) => ({
16392
- ...credential.workosOrganizationId === void 0 ? {} : { workosOrganizationId: credential.workosOrganizationId },
16393
- ...credential.organizationId === void 0 ? {} : { organizationId: credential.organizationId }
16394
- });
16395
- var publicWorkspace = (organization) => ({
16396
- id: organization.id,
16397
- name: organization.name
16398
- });
16399
-
16400
16788
  // packages/contracts/dist/feedback.js
16401
16789
  var FEEDBACK_HISTORY_PAGE_SIZE = 25;
16402
16790
  var FEEDBACK_REPLY_MAX_LENGTH = 2e3;
@@ -16923,7 +17311,7 @@ var PROBLEM_POLICY = {
16923
17311
  github_connection_required: {
16924
17312
  status: 409,
16925
17313
  retryable: false,
16926
- action: "Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for this workspace, complete its browser link, then repeat the original source link."
17314
+ action: "This workspace has no GitHub connection yet. Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for it, complete its browser link, then repeat the original source link. If the project belongs to another account, use that account's login instead; error.acting_as names the login this ran as."
16927
17315
  },
16928
17316
  github_connection_revoked: {
16929
17317
  status: 409,
@@ -16997,7 +17385,11 @@ var PROBLEM_POLICY = {
16997
17385
  retryable: false,
16998
17386
  action: "Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace."
16999
17387
  },
17000
- resource_not_found: { status: 404, retryable: false, action: "Check the resource identifier." },
17388
+ resource_not_found: {
17389
+ status: 404,
17390
+ retryable: false,
17391
+ action: "Check the resource identifier and the account: a project is visible only to its own organization, and error.acting_as names the login this ran as. Use the login of the account that owns it."
17392
+ },
17001
17393
  idempotency_key_reused: {
17002
17394
  status: 409,
17003
17395
  retryable: false,
@@ -17488,7 +17880,7 @@ var parseAccountProfile = (value) => {
17488
17880
  });
17489
17881
  if (new Set(workspaces.map((workspace) => workspace.id)).size !== organizationIds.length)
17490
17882
  throw new ResponseContractError();
17491
- return workspaces;
17883
+ return { userId: input["user_id"], organizations: workspaces };
17492
17884
  };
17493
17885
  var parseProject = (value) => {
17494
17886
  const input = exactRecord2(value, [
@@ -17842,7 +18234,7 @@ var parseOperation = (value) => {
17842
18234
  const blockingOperationId = input["blocking_operation_id"];
17843
18235
  if (blockingOperationId !== void 0 && (state !== "queued" || !isUlid(blockingOperationId) || blockingOperationId === id3))
17844
18236
  throw new ResponseContractError();
17845
- if (!isUlid(id3) || !isOperationState(state) || !isIsoInstant(createdAt) || !isIsoInstant(updatedAt) || result !== void 0 && !isRecord(result)) {
18237
+ if (!isUlid(id3) || !isOperationState(state) || !isIsoInstant(createdAt) || !isIsoInstant(updatedAt) || result !== void 0 && !isRecord2(result)) {
17846
18238
  throw new ResponseContractError();
17847
18239
  }
17848
18240
  let progress;
@@ -17903,12 +18295,12 @@ function parseOperationFailure(value) {
17903
18295
  return safe;
17904
18296
  }
17905
18297
  var exactRecord2 = (value, allowed) => {
17906
- if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) {
18298
+ if (!isRecord2(value) || Object.keys(value).some((key) => !allowed.includes(key))) {
17907
18299
  throw new ResponseContractError();
17908
18300
  }
17909
18301
  return value;
17910
18302
  };
17911
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18303
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17912
18304
  var boundedString = (value, minimum, maximum) => typeof value === "string" && value.length >= minimum && value.length <= maximum;
17913
18305
  var isUlid = (value) => typeof value === "string" && /^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value);
17914
18306
  var isIsoInstant = (value) => typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
@@ -17936,8 +18328,81 @@ var isPaidValidationRecord = (value) => {
17936
18328
  };
17937
18329
  var isHttpsUrlValue = (value) => typeof value === "string" && isHttpsUrl(value);
17938
18330
 
18331
+ // apps/product-cli/dist/workspace.js
18332
+ var InteractiveSessionRequiredError = class extends Error {
18333
+ constructor() {
18334
+ super("Workspace selection requires an interactive login");
18335
+ this.name = "InteractiveSessionRequiredError";
18336
+ }
18337
+ };
18338
+ var ProfileOrganizationFixedError = class extends Error {
18339
+ profile;
18340
+ organization;
18341
+ constructor(profile, organization) {
18342
+ super("The saved login is bound to another organization");
18343
+ this.profile = profile;
18344
+ this.organization = organization;
18345
+ this.name = "ProfileOrganizationFixedError";
18346
+ }
18347
+ };
18348
+ var AccountVerificationError = class extends Error {
18349
+ constructor() {
18350
+ super("The signed-in account does not match the requested one");
18351
+ this.name = "AccountVerificationError";
18352
+ }
18353
+ };
18354
+ var chooseWorkspace = (organizations, requested) => {
18355
+ if (requested !== void 0) {
18356
+ const match = organizations.find((organization) => organization.id === requested);
18357
+ return match === void 0 ? { outcome: "not_a_member", organizations } : { outcome: "selected", organization: match };
18358
+ }
18359
+ if (organizations.length === 0)
18360
+ return { outcome: "no_workspace" };
18361
+ const only = organizations[0];
18362
+ if (organizations.length === 1 && only !== void 0)
18363
+ return { outcome: "selected", organization: only };
18364
+ return { outcome: "choice_required", organizations };
18365
+ };
18366
+ var verifiedUserId = (identity) => {
18367
+ const actor = parseCurrentIdentity(identity).actor_id;
18368
+ if (!actor.startsWith("user:") || actor.length === 5)
18369
+ throw new AccountVerificationError();
18370
+ return actor.slice(5);
18371
+ };
18372
+ var assertVerifiedLogin = (identity, userId, organizationId) => {
18373
+ const organizations = parseCurrentIdentity(identity).organization_ids;
18374
+ if (verifiedUserId(identity) !== userId || (organizationId === null ? organizations.length !== 0 : organizations.length !== 1 || organizations[0] !== organizationId))
18375
+ throw new AccountVerificationError();
18376
+ };
18377
+ var selectWorkspace = async (session, organization, signal, identity) => {
18378
+ if (session.profile.organizationId === organization.id)
18379
+ return session.profile;
18380
+ if (session.profile.organizationId !== null)
18381
+ throw new ProfileOrganizationFixedError(session.profile, organization);
18382
+ const stored = await session.store.read();
18383
+ if (stored === void 0)
18384
+ throw new InteractiveSessionRequiredError();
18385
+ const refreshed = await session.refresher.refresh(stored.refreshToken, signal, organization.workos_id);
18386
+ assertVerifiedLogin(await identity(refreshed.accessToken), session.profile.userId, organization.id);
18387
+ const bound = {
18388
+ ...session.profile,
18389
+ organizationId: organization.id,
18390
+ organizationName: organization.name
18391
+ };
18392
+ await session.profiles.save(bound, { ...refreshed, workosOrganizationId: organization.workos_id, organizationId: organization.id }, session.profile);
18393
+ return bound;
18394
+ };
18395
+ var workspaceOf = (credential) => ({
18396
+ ...credential.workosOrganizationId === void 0 ? {} : { workosOrganizationId: credential.workosOrganizationId },
18397
+ ...credential.organizationId === void 0 ? {} : { organizationId: credential.organizationId }
18398
+ });
18399
+ var publicWorkspace = (organization) => ({
18400
+ id: organization.id,
18401
+ name: organization.name
18402
+ });
18403
+
17939
18404
  // apps/product-cli/dist/cli.js
17940
- import { open as open2 } from "node:fs/promises";
18405
+ import { open as open3 } from "node:fs/promises";
17941
18406
 
17942
18407
  // packages/contracts/dist/database-access.js
17943
18408
  function parseProjectDatabaseWrite(value) {
@@ -18089,7 +18554,7 @@ function exact2(v, keys) {
18089
18554
  // apps/product-cli/package.json
18090
18555
  var package_default = {
18091
18556
  name: "@ohmyhost/product-cli",
18092
- version: "0.1.18",
18557
+ version: "0.1.20",
18093
18558
  private: true,
18094
18559
  ohmyhost: {
18095
18560
  deployment: "production",
@@ -18259,47 +18724,6 @@ function assertRecharge(value) {
18259
18724
  assertBillingTaxIssue(v["billing_issue"]);
18260
18725
  }
18261
18726
 
18262
- // packages/workos-auth-contracts/dist/parsing.js
18263
- var WorkosContractError = class extends Error {
18264
- code;
18265
- constructor(code, message) {
18266
- super(message);
18267
- this.code = code;
18268
- this.name = "WorkosContractError";
18269
- }
18270
- };
18271
- var expectRecord = (value, code = "invalid_object") => {
18272
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
18273
- throw new WorkosContractError(code, "WorkOS response must be an object");
18274
- }
18275
- return value;
18276
- };
18277
- function expectString(value, field, options = {}) {
18278
- if (value === void 0 && options.optional === true)
18279
- return void 0;
18280
- if (typeof value !== "string" || value.length === 0) {
18281
- throw new WorkosContractError("invalid_field", `WorkOS field ${field} must be a non-empty string`);
18282
- }
18283
- return value;
18284
- }
18285
- function expectPositiveInteger(value, field, options = {}) {
18286
- if (value === void 0 && options.optional === true)
18287
- return void 0;
18288
- if (!Number.isSafeInteger(value) || value <= 0) {
18289
- throw new WorkosContractError("invalid_field", `WorkOS field ${field} must be positive`);
18290
- }
18291
- return value;
18292
- }
18293
- var parseJsonObject = (body) => {
18294
- try {
18295
- return expectRecord(JSON.parse(body));
18296
- } catch (error) {
18297
- if (error instanceof WorkosContractError)
18298
- throw error;
18299
- throw new WorkosContractError("invalid_json", "WorkOS returned invalid JSON");
18300
- }
18301
- };
18302
-
18303
18727
  // apps/product-cli/dist/device-token-expiry.js
18304
18728
  var DeviceTokenExpiryError = class extends Error {
18305
18729
  constructor() {
@@ -18420,9 +18844,10 @@ var PsqlUnavailableError = class extends Error {
18420
18844
  var CLI_VERSION = package_default.version;
18421
18845
  var HELP_USAGE = Object.freeze({
18422
18846
  init: "ohmyhost init [--directory PATH] [--root PATH] [--project SLUG] [--region us|eu] [--dry-run] --json (pass the project's hosting region so storage.jurisdiction matches it; us when omitted)",
18423
- login: "ohmyhost login [--organization ULID] --json",
18424
- logout: "ohmyhost logout [--revoke] --json",
18425
- whoami: "ohmyhost whoami --json",
18847
+ login: "ohmyhost login [--organization ULID] [--user USER_ID] [--profile-name NAME] --json (adds one saved login; nothing is saved unless the browser signed in as that user and organization)",
18848
+ logout: "ohmyhost logout [--profile-name NAME] [--revoke] --json (removes only the selected saved login)",
18849
+ whoami: "ohmyhost whoami [--profile-name NAME] --json (the effective user, organization and saved login)",
18850
+ "profile list": "ohmyhost profile list --json (saved logins on this computer: name, user and organization, never a token; pass --profile-name NAME or set OHMYHOST_PROFILE to choose one)",
18426
18851
  "github connect": "ohmyhost github connect --organization ULID --idempotency-key KEY --json (connect once, then link covered repositories without another browser consent)",
18427
18852
  "github status": "ohmyhost github status --organization ULID --json",
18428
18853
  "export create": "ohmyhost export create --project ULID --idempotency-key KEY --stdin --json (password on stdin only; one accepted SQL ZIP per project per 24 hours)",
@@ -18437,9 +18862,9 @@ var HELP_USAGE = Object.freeze({
18437
18862
  "credits usage": "ohmyhost credits usage --organization ULID --month YYYY-MM [--cursor ULID] --json",
18438
18863
  "budget get": "ohmyhost budget get --project ULID --json",
18439
18864
  "budget set": "ohmyhost budget set --project ULID --credits NUMBER|none [--mode continue|stop] --idempotency-key KEY --json",
18440
- "organization create": "ohmyhost organization create --name NAME --idempotency-key KEY [--source SOURCE] --json (SOURCE is optional attribution from a link's r value; the new workspace is selected immediately)",
18441
- "organization list": "ohmyhost organization list --json (the workspaces you belong to and the selected one)",
18442
- "organization use": "ohmyhost organization use --organization ULID --json",
18865
+ "organization create": "ohmyhost organization create --name NAME --idempotency-key KEY [--source SOURCE] --json (SOURCE is optional attribution from a link's r value; a login without organization is bound to the new workspace, another login keeps its own)",
18866
+ "organization list": "ohmyhost organization list [--profile-name NAME] --json (the workspaces of the chosen login's user and the one that login is scoped to)",
18867
+ "organization use": "ohmyhost organization use --organization ULID [--profile-name NAME] --json (binds a login that has no organization yet; another organization needs its own login)",
18443
18868
  "operation get": "ohmyhost operation get OPERATION_ULID --json",
18444
18869
  "operation reconcile": "ohmyhost operation reconcile OPERATION_ULID --idempotency-key KEY --yes --json",
18445
18870
  "token create": "ohmyhost token create --organization ULID --name NAME --idempotency-key KEY --out .env.local --json",
@@ -18489,7 +18914,7 @@ var HELP_USAGE = Object.freeze({
18489
18914
  delete: "ohmyhost delete --project ULID --if-match ETAG --confirmation-token TOKEN --idempotency-key KEY --yes --json",
18490
18915
  "secret list": "ohmyhost secret list --project ULID --environment ENVIRONMENT_ULID --json",
18491
18916
  "function runs": "ohmyhost function runs --project ULID --environment ENVIRONMENT_ULID [--limit 1-100] --json",
18492
- "secret set": `printf '%s' "$SECRET_VALUE" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY --stdin [--wait] --json`,
18917
+ "secret set": `printf '%s' "$SECRET_VALUE" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--profile-user USER_ID --profile-organization ULID | --token-user USER_ID --token-organization ULID] --stdin [--wait] --json (with --profile-user and --profile-organization the saved login that runs it must belong to that user and organization; with --token-user and --token-organization it runs only with an OHMYHOST_TOKEN of that user and organization, never with a saved login)`,
18493
18918
  "secret delete": "ohmyhost secret delete NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--wait] --json",
18494
18919
  "mail setup": "ohmyhost mail setup --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
18495
18920
  "mail status": "ohmyhost mail status --project ULID --environment ULID --json",
@@ -18507,8 +18932,8 @@ var HELP_COMMANDS = Object.freeze(Object.entries(HELP_USAGE).map(([command, usag
18507
18932
  var cliMetadataResult = (kind, topic) => kind === "help" ? success("help", {
18508
18933
  status: "succeeded",
18509
18934
  cli_version: CLI_VERSION,
18510
- usage: "ohmyhost <command> [options] --json",
18511
- environment: "OHMYHOST_ENVIRONMENT=development|production (default: production)",
18935
+ usage: "ohmyhost <command> [options] [--profile-name NAME] --json",
18936
+ environment: "OHMYHOST_ENVIRONMENT=development|production (default: production); OHMYHOST_PROFILE=NAME binds this process to one saved login",
18512
18937
  commands: HELP_COMMANDS.filter((entry) => !topic || entry.command === topic)
18513
18938
  }) : success("version", { status: "succeeded", cli_version: CLI_VERSION });
18514
18939
  var ProductCliFailure = class extends Error {
@@ -18529,8 +18954,11 @@ var ProductCliFailure = class extends Error {
18529
18954
  };
18530
18955
  var ProductCli = class {
18531
18956
  dependencies;
18957
+ /** The saved login of this run; binding a login without organization replaces it. */
18958
+ #profile;
18532
18959
  constructor(dependencies) {
18533
18960
  this.dependencies = dependencies;
18961
+ this.#profile = dependencies.profile;
18534
18962
  }
18535
18963
  async run(argv, signal = new AbortController().signal) {
18536
18964
  let command;
@@ -18560,8 +18988,10 @@ var ProductCli = class {
18560
18988
  if (this.dependencies.apiToken !== void 0 && (command.kind === "organization-create" || command.kind === "organization-list" || command.kind === "organization-use")) {
18561
18989
  throw new InteractiveSessionRequiredError();
18562
18990
  }
18991
+ if (command.kind === "profile-list")
18992
+ return await this.listProfiles();
18563
18993
  if (command.kind === "login")
18564
- return await this.login(command.credentialStore, command.organizationId, signal);
18994
+ return await this.login(command.organizationId, command.userId, signal);
18565
18995
  if (command.kind === "logout") {
18566
18996
  return await this.logout(command.credentialStore, command.revoke, signal);
18567
18997
  }
@@ -18672,35 +19102,48 @@ var ProductCli = class {
18672
19102
  ...command.signupSource === void 0 ? {} : { signupSource: command.signupSource },
18673
19103
  idempotencyKey: command.idempotencyKey
18674
19104
  }));
18675
- const selection = await this.chooseAndStoreWorkspace(await api.getAccountProfile(), organization.id, command.credentialStore, signal);
19105
+ const selection = chooseWorkspace((await api.getAccountProfile()).organizations, organization.id);
18676
19106
  if (selection.outcome !== "selected")
18677
19107
  throw new ProductCliFailure(2, "organization_not_found", true, `The new workspace is not yet readable for this account. Run '${this.dependencies.commandPrefix} organization use --organization ${organization.id} --json' again.`);
19108
+ const current = this.#profile;
19109
+ if (current !== void 0 && current.organizationId !== null && current.organizationId !== organization.id)
19110
+ return success("organization create", {
19111
+ status: "succeeded",
19112
+ organization,
19113
+ selected: current.organizationId,
19114
+ profile: publicProfile(current),
19115
+ next_action: `Run '${this.dependencies.commandPrefix} login --organization ${organization.id} --user ${current.userId} --json' to add a login for the new workspace as the same user; profile ${current.name} stays in organization ${current.organizationId}.`
19116
+ });
19117
+ const profile = await this.bindOrganization(selection.organization, signal);
18678
19118
  return success("organization create", {
18679
19119
  status: "succeeded",
18680
19120
  organization,
18681
19121
  selected: organization.id,
19122
+ profile: publicProfile(profile),
18682
19123
  next_action: `Run '${this.dependencies.commandPrefix} whoami --json' to confirm the selected workspace, then create a project.`,
18683
19124
  ...await this.githubNextAction(await this.authenticatedApi(command.credentialStore, signal), organization.id)
18684
19125
  });
18685
19126
  }
18686
19127
  if (command.kind === "organization-list") {
18687
19128
  const api = await this.authenticatedApi(command.credentialStore, signal);
18688
- const organizations = await api.getAccountProfile();
19129
+ const account = await api.getAccountProfile();
18689
19130
  return success("organization list", {
18690
19131
  status: "listed",
18691
- organizations: organizations.map(publicWorkspace),
18692
- selected: await selectedWorkspaceId(this.workspaceSession(command.credentialStore))
19132
+ organizations: account.organizations.map(publicWorkspace),
19133
+ selected: this.#profile?.organizationId ?? null
18693
19134
  });
18694
19135
  }
18695
19136
  if (command.kind === "organization-use") {
18696
19137
  const api = await this.authenticatedApi(command.credentialStore, signal);
18697
- const selection = await this.chooseAndStoreWorkspace(await api.getAccountProfile(), command.organizationId, command.credentialStore, signal);
19138
+ const selection = chooseWorkspace((await api.getAccountProfile()).organizations, command.organizationId);
18698
19139
  if (selection.outcome !== "selected")
18699
19140
  throw this.unknownWorkspace(selection);
19141
+ const profile = await this.bindOrganization(selection.organization, signal);
18700
19142
  return success("organization use", {
18701
19143
  status: "selected",
18702
19144
  organization: publicWorkspace(selection.organization),
18703
19145
  selected: selection.organization.id,
19146
+ profile: publicProfile(profile),
18704
19147
  ...await this.githubNextAction(await this.authenticatedApi(command.credentialStore, signal), selection.organization.id)
18705
19148
  });
18706
19149
  }
@@ -18788,7 +19231,7 @@ var ProductCli = class {
18788
19231
  return success(command.kind === "project-dev-share-link" ? "project dev-share link" : command.kind === "project-dev-share-rotate" ? "project dev-share rotate" : command.kind === "project-dev-share-revoke" ? "project dev-share revoke" : "project dev-access mode", { status: "succeeded", state });
18789
19232
  }
18790
19233
  if (command.kind === "database-write") {
18791
- const file = await open2(command.statementFile, "r"), buffer = Buffer.alloc(65537);
19234
+ const file = await open3(command.statementFile, "r"), buffer = Buffer.alloc(65537);
18792
19235
  let statement;
18793
19236
  try {
18794
19237
  const stat3 = await file.stat();
@@ -18901,8 +19344,10 @@ var ProductCli = class {
18901
19344
  return failure(name, 3, error.code, false, error.message);
18902
19345
  if (error instanceof InteractiveSessionRequiredError)
18903
19346
  return failure(name, 2, "interactive_login_required", false, `A user API token cannot create, list or select a workspace. Run '${this.dependencies.commandPrefix} login --json' in a process without OHMYHOST_TOKEN.`);
19347
+ if (error instanceof ProfileLimitError)
19348
+ return failure(name, 2, "profile_limit_reached", false, profileLimitAction(this.dependencies.commandPrefix));
18904
19349
  this.recordDiagnostic(diagnosticDescriptor(error, signal));
18905
- return classifyFailure(name, error, signal);
19350
+ return classifyFailure(name, error, signal, this.context());
18906
19351
  }
18907
19352
  }
18908
19353
  githubConnectAction(organizationId) {
@@ -18925,22 +19370,53 @@ var ProductCli = class {
18925
19370
  return success("whoami", {
18926
19371
  status: "authenticated",
18927
19372
  identity,
19373
+ context: this.context(),
18928
19374
  ...identity.organization_ids.length === 1 && identity.organization_ids[0] !== void 0 ? await this.githubNextAction(api, identity.organization_ids[0]) : {}
18929
19375
  });
18930
- const selection = await this.chooseAndStoreWorkspace(await api.getAccountProfile(), void 0, command.credentialStore, signal);
18931
- if (selection.outcome === "selected")
19376
+ const selection = chooseWorkspace((await api.getAccountProfile()).organizations, void 0);
19377
+ if (selection.outcome === "selected") {
19378
+ await this.bindOrganization(selection.organization, signal);
18932
19379
  return success("whoami", {
18933
19380
  status: "authenticated",
18934
19381
  identity: parseCurrentIdentity(await api.getCurrentIdentity()),
18935
19382
  organization: publicWorkspace(selection.organization),
18936
19383
  selected: selection.organization.id,
19384
+ context: this.context(),
18937
19385
  ...await this.githubNextAction(api, selection.organization.id)
18938
19386
  });
19387
+ }
18939
19388
  return success("whoami", {
18940
19389
  status: "authenticated",
18941
19390
  identity,
19391
+ context: this.context(),
18942
19392
  organizations: selection.outcome === "choice_required" ? selection.organizations.map(publicWorkspace) : [],
18943
- next_action: selection.outcome === "choice_required" ? `Ask the customer which workspace to use, then run '${this.dependencies.commandPrefix} organization use --organization ULID --json'.` : `Run '${this.dependencies.commandPrefix} organization create --name NAME --idempotency-key KEY --json' to create the first workspace.`
19393
+ next_action: this.workspaceAction(selection)
19394
+ });
19395
+ }
19396
+ /** Which account this run acts as, without any credential. */
19397
+ context() {
19398
+ if (this.dependencies.apiToken !== void 0)
19399
+ return { credential: "environment_token" };
19400
+ return {
19401
+ credential: "profile",
19402
+ profile: this.#profile === void 0 ? null : publicProfile(this.#profile)
19403
+ };
19404
+ }
19405
+ /** The next step for a login that has no organization yet. */
19406
+ workspaceAction(selection) {
19407
+ const prefix = this.dependencies.commandPrefix;
19408
+ const named = this.#profile === void 0 ? "" : ` --profile-name ${this.#profile.name}`;
19409
+ return selection.outcome === "choice_required" ? `Ask the customer which workspace to use, then run '${prefix} organization use --organization ULID${named} --json'.` : `Run '${prefix} organization create --name NAME --idempotency-key KEY${named} --json' to create the first workspace.`;
19410
+ }
19411
+ async listProfiles() {
19412
+ const profiles = this.dependencies.profiles;
19413
+ if (profiles === void 0)
19414
+ throw new ProviderCapabilityBlockedError();
19415
+ return success("profile list", {
19416
+ status: "listed",
19417
+ profiles: (await profiles.list()).map(publicProfile),
19418
+ bound: this.dependencies.profileBinding ?? null,
19419
+ environment_token: this.dependencies.apiToken !== void 0
18944
19420
  });
18945
19421
  }
18946
19422
  recordDiagnostic(event) {
@@ -18953,7 +19429,11 @@ var ProductCli = class {
18953
19429
  }
18954
19430
  }
18955
19431
  // The reviewed boundary proof parses this signature, so keep its parameters brace-free.
18956
- async login(credentialStore, organizationId, signal) {
19432
+ async login(organizationId, expectedUserId, signal) {
19433
+ const profiles = this.dependencies.profiles;
19434
+ const readIdentity = this.dependencies.currentIdentity;
19435
+ if (profiles === void 0 || readIdentity === void 0)
19436
+ throw new ProviderCapabilityBlockedError();
18957
19437
  const signInDeadline = this.dependencies.now().getTime() + SIGN_IN_WINDOW_MS;
18958
19438
  let session = await this.dependencies.deviceFlow.authorize();
18959
19439
  await presentAuthorization(this.dependencies.deviceAuthorizationPresenter, session.authorization);
@@ -18990,53 +19470,112 @@ var ProductCli = class {
18990
19470
  throw error;
18991
19471
  throw new ProductCliFailure(4, "device_token_expiry_missing", false, "Run login again; the authorization response was incomplete.");
18992
19472
  }
18993
- await this.dependencies.credentialStore(credentialStore).replace({
19473
+ const signedIn = {
18994
19474
  accessToken: result.tokens.accessToken,
18995
19475
  refreshToken: result.tokens.refreshToken,
18996
19476
  expiresAt
18997
- });
18998
- const profile = await this.dependencies.accountProfile(() => this.authenticatedToken(credentialStore, signal));
18999
- const selection = await this.chooseAndStoreWorkspace(profile, organizationId, credentialStore, signal);
19477
+ };
19478
+ const account = await this.dependencies.accountProfile(async () => signedIn.accessToken);
19479
+ if (!isProfileUserId(account.userId))
19480
+ throw this.unverifiedLogin();
19481
+ if (expectedUserId !== void 0 && account.userId !== expectedUserId)
19482
+ throw new ProductCliFailure(4, "login_account_mismatch", false, `The browser signed in as user ${account.userId}, not ${expectedUserId}. Nothing was saved and every saved login is unchanged. Sign out of ohmyho.st in that browser or use the browser profile of ${expectedUserId}, then run '${this.dependencies.commandPrefix} login${organizationId === void 0 ? "" : ` --organization ${organizationId}`} --user ${expectedUserId} --json' again.`);
19483
+ const selection = chooseWorkspace(account.organizations, organizationId);
19484
+ if (selection.outcome === "not_a_member")
19485
+ throw this.unknownWorkspace(selection);
19486
+ const credential = selection.outcome === "selected" ? await runBounded((bounded2) => this.dependencies.tokenRefresher.refresh(signedIn.refreshToken, bounded2, selection.organization.workos_id), signal, this.dependencies.lifecycleTimeoutMs) : signedIn;
19487
+ const identity = await readIdentity(async () => credential.accessToken);
19488
+ const scopedId = selection.outcome === "selected" ? selection.organization.id : parseCurrentIdentity(identity).organization_ids[0] ?? null;
19489
+ const organization = scopedId === null ? null : account.organizations.find((candidate) => candidate.id === scopedId);
19490
+ if (organization === void 0)
19491
+ throw this.unverifiedLogin();
19492
+ try {
19493
+ assertVerifiedLogin(identity, account.userId, scopedId);
19494
+ } catch (error) {
19495
+ if (error instanceof AccountVerificationError)
19496
+ throw this.unverifiedLogin();
19497
+ throw error;
19498
+ }
19499
+ const login = {
19500
+ userId: account.userId,
19501
+ organizationId: scopedId,
19502
+ organizationName: organization?.name ?? null
19503
+ };
19504
+ const profile = {
19505
+ name: this.dependencies.profileName ?? defaultProfileName(await profiles.list(), login),
19506
+ ...login
19507
+ };
19508
+ try {
19509
+ await profiles.save(profile, organization === null ? credential : {
19510
+ ...credential,
19511
+ workosOrganizationId: organization.workos_id,
19512
+ organizationId: organization.id
19513
+ });
19514
+ } catch (error) {
19515
+ if (!(error instanceof ProfileNameConflictError))
19516
+ throw error;
19517
+ throw new ProductCliFailure(2, "profile_name_conflict", false, `The name ${profile.name} already belongs to the saved login of user ${error.profile.userId} in organization ${String(error.profile.organizationId ?? "none")}. Nothing was saved. Choose another --profile-name, or remove that login with '${this.dependencies.commandPrefix} logout --profile-name ${profile.name} --json' first.`);
19518
+ }
19519
+ this.#profile = profile;
19000
19520
  const readGithubConnection = this.dependencies.githubConnectionStatus;
19001
- if (selection.outcome === "selected")
19521
+ if (organization !== null)
19002
19522
  return success("login", {
19003
19523
  status: "authenticated",
19004
- organization: publicWorkspace(selection.organization),
19524
+ organization: publicWorkspace(organization),
19525
+ profile: publicProfile(profile),
19005
19526
  ...readGithubConnection === void 0 ? {} : await this.githubNextAction({
19006
- getGithubConnection: (selectedOrganizationId) => readGithubConnection(() => this.authenticatedToken(credentialStore, signal), selectedOrganizationId)
19007
- }, selection.organization.id)
19527
+ getGithubConnection: (selectedOrganizationId) => readGithubConnection(async () => credential.accessToken, selectedOrganizationId)
19528
+ }, organization.id)
19008
19529
  });
19009
- if (selection.outcome === "not_a_member")
19010
- throw this.unknownWorkspace(selection);
19011
19530
  return success("login", {
19012
19531
  status: "authenticated",
19013
19532
  organization: null,
19533
+ profile: publicProfile(profile),
19014
19534
  organizations: selection.outcome === "choice_required" ? selection.organizations.map(publicWorkspace) : [],
19015
- next_action: selection.outcome === "choice_required" ? `Ask the customer which workspace to use, then run '${this.dependencies.commandPrefix} organization use --organization ULID --json'.` : `Run '${this.dependencies.commandPrefix} organization create --name NAME --idempotency-key KEY --json' to create the first workspace.`
19535
+ next_action: this.workspaceAction(selection)
19016
19536
  });
19017
19537
  }
19018
19538
  }
19019
- workspaceSession(credentialStore) {
19020
- return {
19021
- store: this.dependencies.credentialStore(credentialStore),
19022
- refresher: this.dependencies.tokenRefresher
19023
- };
19024
- }
19025
- /** Decide the workspace and, when the answer cannot be wrong, put it into the credential. */
19026
- async chooseAndStoreWorkspace(organizations, requested, credentialStore, signal) {
19027
- const selection = chooseWorkspace(organizations, requested);
19028
- if (selection.outcome === "selected")
19029
- await runBounded((bounded2) => selectWorkspace(this.workspaceSession(credentialStore), selection.organization, bounded2), signal, this.dependencies.lifecycleTimeoutMs);
19030
- return selection;
19539
+ /** The provider answered with another account than the one this login requested. */
19540
+ unverifiedLogin() {
19541
+ return new ProductCliFailure(4, "login_verification_failed", true, `The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login --json' again.`);
19542
+ }
19543
+ /**
19544
+ * Bind the login of this run, which has no organization yet, to one of its user's organizations.
19545
+ * A login that already has one keeps it; another organization is its own login.
19546
+ */
19547
+ async bindOrganization(organization, signal) {
19548
+ const profiles = this.dependencies.profiles;
19549
+ const profile = this.#profile;
19550
+ if (profiles === void 0 || profile === void 0)
19551
+ throw new InteractiveSessionRequiredError();
19552
+ let bound;
19553
+ try {
19554
+ bound = await runBounded((bounded2) => selectWorkspace({
19555
+ store: profiles.credential(profile),
19556
+ refresher: this.dependencies.tokenRefresher,
19557
+ profiles,
19558
+ profile
19559
+ }, organization, bounded2, (accessToken) => this.dependencies.productApi(async () => accessToken).getCurrentIdentity()), signal, this.dependencies.lifecycleTimeoutMs);
19560
+ } catch (error) {
19561
+ if (error instanceof AccountVerificationError)
19562
+ throw this.unverifiedLogin();
19563
+ if (!(error instanceof ProfileOrganizationFixedError))
19564
+ throw error;
19565
+ throw new ProductCliFailure(2, "profile_organization_fixed", false, `Profile ${profile.name} stays signed in to organization ${String(profile.organizationId)}. Run '${this.dependencies.commandPrefix} login --organization ${organization.id} --user ${profile.userId} --json' to add a separate login for ${organization.id} as the same user; a saved login never switches organizations.`);
19566
+ }
19567
+ this.#profile = bound;
19568
+ return bound;
19031
19569
  }
19032
19570
  unknownWorkspace(selection) {
19033
19571
  return new ProductCliFailure(2, "organization_not_found", false, `You are not a member of that workspace. Run '${this.dependencies.commandPrefix} organization list --json' and choose one of ${String(selection.outcome === "not_a_member" ? selection.organizations.length : 0)}.`);
19034
19572
  }
19035
19573
  async logout(credentialStore, revoke, signal) {
19036
- const store = this.dependencies.credentialStore(credentialStore);
19574
+ const store = this.store(credentialStore);
19575
+ const removed = this.#profile === void 0 ? {} : { profile: publicProfile(this.#profile) };
19037
19576
  if (!revoke) {
19038
- await store.clear();
19039
- return success("logout", { status: "logged_out" });
19577
+ await this.forgetLogin(store);
19578
+ return success("logout", { status: "logged_out", ...removed });
19040
19579
  }
19041
19580
  const credential = await store.read();
19042
19581
  let revocationError;
@@ -19047,7 +19586,7 @@ var ProductCli = class {
19047
19586
  } catch (error) {
19048
19587
  revocationError = error;
19049
19588
  } finally {
19050
- await store.clear();
19589
+ await this.forgetLogin(store);
19051
19590
  }
19052
19591
  if (revocationError !== void 0) {
19053
19592
  if (isCancellation(revocationError) && signal.aborted)
@@ -19063,7 +19602,29 @@ var ProductCli = class {
19063
19602
  }
19064
19603
  throw revocationError;
19065
19604
  }
19066
- return success("logout", { status: "logged_out" });
19605
+ return success("logout", { status: "logged_out", ...removed });
19606
+ }
19607
+ /** The credential store of this run's saved login; it follows a binding made during the run. */
19608
+ store(kind) {
19609
+ const profiles = this.dependencies.profiles;
19610
+ if (profiles !== void 0 && this.#profile !== void 0)
19611
+ return profiles.credential(this.#profile);
19612
+ if (this.dependencies.credentialStore === void 0)
19613
+ throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
19614
+ return this.dependencies.credentialStore(kind);
19615
+ }
19616
+ /** Removes only this run's saved login; every other saved login stays untouched. */
19617
+ async forgetLogin(store) {
19618
+ const profiles = this.dependencies.profiles;
19619
+ if (profiles !== void 0 && this.#profile !== void 0)
19620
+ await profiles.remove(this.#profile);
19621
+ else
19622
+ await store.clear();
19623
+ }
19624
+ /** How to sign this run's login in again, naming its organization and alias when known. */
19625
+ loginAction() {
19626
+ const profile = this.#profile;
19627
+ return `Run '${this.dependencies.commandPrefix} login${profile?.organizationId === void 0 || profile.organizationId === null ? "" : ` --organization ${profile.organizationId}`}${profile === void 0 ? "" : ` --profile-name ${profile.name}`} --json'.`;
19067
19628
  }
19068
19629
  async getOperation(command, signal) {
19069
19630
  const api = await this.authenticatedApi(command.credentialStore, signal);
@@ -19116,7 +19677,7 @@ var ProductCli = class {
19116
19677
  async getProjectStatus(command, signal) {
19117
19678
  const api = await this.authenticatedApi(command.credentialStore, signal);
19118
19679
  const project = parseProjectStatus(await api.getProjectStatus(command.projectId));
19119
- return success("project status", { status: "succeeded", project });
19680
+ return success("project status", { status: "succeeded", project, context: this.context() });
19120
19681
  }
19121
19682
  async createDevAccessTicket(command, signal) {
19122
19683
  const api = await this.authenticatedApi(command.credentialStore, signal);
@@ -19259,14 +19820,18 @@ var ProductCli = class {
19259
19820
  const source = project.source;
19260
19821
  if (project.project_id !== command.projectId || source === null || source.status !== "ready" || source.repository_full_name.toLowerCase() !== `${command.repositoryOwner}/${command.repositoryName}`.toLowerCase())
19261
19822
  throw new ResponseContractError();
19823
+ const [organizationId, ...others] = parseCurrentIdentity(await api.getCurrentIdentity()).organization_ids;
19824
+ if (organizationId === void 0 || others.length > 0)
19825
+ throw new ResponseContractError();
19262
19826
  await this.dependencies.projectLinkStore.save({
19263
- version: 1,
19827
+ version: 2,
19264
19828
  api_origin: this.dependencies.productApiOrigin,
19829
+ organization_id: organizationId,
19265
19830
  project_id: command.projectId,
19266
19831
  installation_id: source.installation_id,
19267
19832
  repository_full_name: source.repository_full_name
19268
19833
  });
19269
- return success("link", { status: "completed", operation });
19834
+ return success("link", { status: "completed", operation, organization_id: organizationId });
19270
19835
  }
19271
19836
  async configureAutoDeploy(command, signal) {
19272
19837
  const api = await this.authenticatedApi(command.credentialStore, signal);
@@ -19509,10 +20074,10 @@ var ProductCli = class {
19509
20074
  async authenticatedToken(credentialStore, signal) {
19510
20075
  if (this.dependencies.apiToken !== void 0)
19511
20076
  return this.dependencies.apiToken;
19512
- const store = this.dependencies.credentialStore(credentialStore);
20077
+ const store = this.store(credentialStore);
19513
20078
  const stored = await store.read();
19514
20079
  if (stored === void 0) {
19515
- throw new ProductCliFailure(5, "authentication_required", false, "Run 'ohmyhost login'.");
20080
+ throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
19516
20081
  }
19517
20082
  let refresh;
19518
20083
  try {
@@ -19520,8 +20085,8 @@ var ProductCli = class {
19520
20085
  } catch (error) {
19521
20086
  if (!(error instanceof TokenRefreshRejectedError))
19522
20087
  throw error;
19523
- await store.clear();
19524
- throw new ProductCliFailure(5, "authentication_required", false, "Run 'ohmyhost login'.");
20088
+ await this.forgetLogin(store);
20089
+ throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
19525
20090
  }
19526
20091
  if (refresh.refreshed)
19527
20092
  await store.replace(refresh.credential);
@@ -19587,7 +20152,16 @@ var redactVerificationUri = (value) => {
19587
20152
  url.hash = "";
19588
20153
  return url.toString();
19589
20154
  };
19590
- var classifyFailure = (command, error, signal) => {
20155
+ var ACCOUNT_SENSITIVE_PROBLEMS = /* @__PURE__ */ new Set([
20156
+ "resource_not_found",
20157
+ "forbidden",
20158
+ "organization_required",
20159
+ "github_connection_required",
20160
+ "github_connection_revoked",
20161
+ "repository_not_installed"
20162
+ ]);
20163
+ var classifyCliFailure = (command, error, signal) => classifyFailure(command, error, signal);
20164
+ var classifyFailure = (command, error, signal, actingAs) => {
19591
20165
  if (signal.aborted || isCancellation(error)) {
19592
20166
  return failure(command, 130, "command_cancelled", true, "Run the command again.");
19593
20167
  }
@@ -19621,7 +20195,7 @@ var classifyFailure = (command, error, signal) => {
19621
20195
  if (looksLikeProblem(error)) {
19622
20196
  try {
19623
20197
  const problem = parseSafeProblem(error);
19624
- return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds, problem.requestId, problem.docsUrl);
20198
+ return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds, problem.requestId, problem.docsUrl, ACCOUNT_SENSITIVE_PROBLEMS.has(problem.code) ? actingAs : void 0);
19625
20199
  } catch {
19626
20200
  return failure(command, 7, "response_contract_invalid", false, "Retry only after the client or server contract is repaired.");
19627
20201
  }
@@ -19641,7 +20215,7 @@ var success = (command, body) => ({
19641
20215
  `,
19642
20216
  stderr: ""
19643
20217
  });
19644
- var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds, requestId, docsUrl) => {
20218
+ var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds, requestId, docsUrl, actingAs) => {
19645
20219
  const document = {
19646
20220
  version: 1,
19647
20221
  command,
@@ -19654,7 +20228,8 @@ var failure = (command, exitCode, code, retryable, suggestedAction, operation, r
19654
20228
  ...retryAfterSeconds === void 0 ? {} : { retry_after_seconds: retryAfterSeconds },
19655
20229
  ...requestId === void 0 ? {} : { request_id: requestId },
19656
20230
  ...docsUrl === void 0 ? {} : { docs_url: docsUrl },
19657
- ...operation?.error === void 0 ? {} : { message: operation.error.message }
20231
+ ...operation?.error === void 0 ? {} : { message: operation.error.message },
20232
+ ...actingAs === void 0 ? {} : { acting_as: actingAs }
19658
20233
  }
19659
20234
  };
19660
20235
  return { exitCode, stdout: `${JSON.stringify(document)}
@@ -19787,6 +20362,8 @@ var parsedCommandName = (command) => {
19787
20362
  return "secret delete";
19788
20363
  if (command.kind === "managed-mail")
19789
20364
  return command.label;
20365
+ if (command.kind === "profile-list")
20366
+ return "profile list";
19790
20367
  return command.kind;
19791
20368
  };
19792
20369
  var boundedSignal = (parent, timeoutMs) => AbortSignal.any([parent, AbortSignal.timeout(timeoutMs)]);
@@ -21224,6 +21801,23 @@ var deleteProject = (parameters, options) => {
21224
21801
  ...params
21225
21802
  });
21226
21803
  };
21804
+ var getProject = (parameters, options) => {
21805
+ const params = buildClientParams([parameters], [
21806
+ {
21807
+ args: [
21808
+ { in: "headers", key: "X-Request-Id" },
21809
+ { in: "path", key: "project_id" }
21810
+ ]
21811
+ }
21812
+ ]);
21813
+ return (options?.client ?? client).get({
21814
+ responseStyle: "data",
21815
+ security: [{ scheme: "bearer", type: "http" }],
21816
+ url: "/v1/projects/{project_id}",
21817
+ ...options,
21818
+ ...params
21819
+ });
21820
+ };
21227
21821
  var getProjectContext = (parameters, options) => {
21228
21822
  const params = buildClientParams([parameters], [
21229
21823
  {
@@ -22908,18 +23502,75 @@ var GeneratedSdkProductApi = class {
22908
23502
  };
22909
23503
 
22910
23504
  // apps/product-cli/dist/project-link-store.js
22911
- import { appendFile, chmod, mkdir as mkdir2, readFile, rename, stat, writeFile } from "node:fs/promises";
22912
- import { join } from "node:path";
22913
- async function readLinkedProjectId(directory, apiOrigin) {
23505
+ import { appendFile, chmod, mkdir as mkdir3, readFile as readFile2, readdir as readdir2, rename, stat, unlink as unlink2, writeFile } from "node:fs/promises";
23506
+ import { join as join2 } from "node:path";
23507
+ var ULID2 = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
23508
+ var LINKS = "links";
23509
+ var LEGACY_LINK = "link.json";
23510
+ async function readProjectLinks(directory, apiOrigin) {
23511
+ let names = [];
22914
23512
  try {
22915
- const link = JSON.parse(await readFile(join(directory, "link.json"), "utf8"));
22916
- if (typeof link !== "object" || link === null)
22917
- return null;
22918
- const { version: version2, api_origin, project_id } = link;
22919
- return version2 === 1 && api_origin === apiOrigin && typeof project_id === "string" && /^[0-9A-HJKMNP-TV-Z]{26}$/u.test(project_id) ? project_id : null;
23513
+ names = await readdir2(join2(directory, LINKS));
22920
23514
  } catch {
22921
- return null;
23515
+ names = [];
22922
23516
  }
23517
+ const links = [];
23518
+ for (const name of names.sort()) {
23519
+ const organizationId = /^([0-7][0-9A-HJKMNP-TV-Z]{25})\.json$/u.exec(name)?.[1];
23520
+ if (organizationId === void 0)
23521
+ continue;
23522
+ const link2 = await readJson(join2(directory, LINKS, name));
23523
+ if (isRecord3(link2, [
23524
+ "version",
23525
+ "api_origin",
23526
+ "organization_id",
23527
+ "project_id",
23528
+ "installation_id",
23529
+ "repository_full_name"
23530
+ ]) && link2["version"] === 2 && link2["api_origin"] === apiOrigin && link2["organization_id"] === organizationId && typeof link2["project_id"] === "string" && ULID2.test(link2["project_id"]) && typeof link2["installation_id"] === "string" && typeof link2["repository_full_name"] === "string")
23531
+ links.push(link2);
23532
+ }
23533
+ const legacy = await readJson(join2(directory, LEGACY_LINK));
23534
+ return {
23535
+ links,
23536
+ legacy: typeof legacy === "object" && legacy !== null && legacy["version"] === 1 && legacy["api_origin"] === apiOrigin && typeof legacy["project_id"] === "string" && ULID2.test(legacy["project_id"]) && typeof legacy["installation_id"] === "string" && typeof legacy["repository_full_name"] === "string" ? {
23537
+ project_id: legacy["project_id"],
23538
+ installation_id: legacy["installation_id"],
23539
+ repository_full_name: legacy["repository_full_name"]
23540
+ } : null
23541
+ };
23542
+ }
23543
+ function chooseLinkedProject(directory, organizationId) {
23544
+ if (organizationId !== void 0) {
23545
+ const link2 = directory.links.find((candidate) => candidate.organization_id === organizationId);
23546
+ if (link2 !== void 0)
23547
+ return { outcome: "linked", projectId: link2.project_id, organizationId };
23548
+ return directory.legacy === null ? { outcome: "none" } : { outcome: "linked", projectId: directory.legacy.project_id, organizationId: null };
23549
+ }
23550
+ const candidates = [
23551
+ ...directory.links.map((link2) => ({
23552
+ projectId: link2.project_id,
23553
+ organizationId: link2.organization_id
23554
+ })),
23555
+ ...directory.legacy === null ? [] : [{ projectId: directory.legacy.project_id, organizationId: null }]
23556
+ ];
23557
+ const [only] = candidates;
23558
+ if (candidates.length === 1 && only !== void 0)
23559
+ return { outcome: "linked", ...only };
23560
+ if (candidates.length === 0)
23561
+ return { outcome: "none" };
23562
+ return {
23563
+ outcome: "ambiguous",
23564
+ organizationIds: directory.links.map((link2) => link2.organization_id)
23565
+ };
23566
+ }
23567
+ function linkedOrganization(directory, projectId) {
23568
+ const organizations = new Set(directory.links.filter((link2) => link2.project_id === projectId).map((link2) => link2.organization_id));
23569
+ const [organizationId] = organizations;
23570
+ return organizations.size === 1 ? organizationId : void 0;
23571
+ }
23572
+ async function removeLegacyProjectLink(directory) {
23573
+ await unlink2(join2(directory, LEGACY_LINK));
22923
23574
  }
22924
23575
  var OwnerOnlyProjectLinkStore = class {
22925
23576
  directory;
@@ -22930,10 +23581,15 @@ var OwnerOnlyProjectLinkStore = class {
22930
23581
  this.repositoryRoot = repositoryRoot;
22931
23582
  }
22932
23583
  async save(metadata) {
22933
- await mkdir2(this.directory, { mode: 448, recursive: true });
22934
- await chmod(this.directory, 448);
22935
- const target = join(this.directory, "link.json");
22936
- const temporary = join(this.directory, `.link-${process.pid}.tmp`);
23584
+ if (!ULID2.test(metadata.organization_id))
23585
+ throw new TypeError("Invalid linked organization");
23586
+ const links = join2(this.directory, LINKS);
23587
+ for (const directory of [this.directory, links]) {
23588
+ await mkdir3(directory, { mode: 448, recursive: true });
23589
+ await chmod(directory, 448);
23590
+ }
23591
+ const target = join2(links, `${metadata.organization_id}.json`);
23592
+ const temporary = join2(links, `.link-${process.pid}.tmp`);
22937
23593
  await writeFile(temporary, `${JSON.stringify(metadata)}
22938
23594
  `, { encoding: "utf8", mode: 384 });
22939
23595
  await chmod(temporary, 384);
@@ -22943,16 +23599,25 @@ var OwnerOnlyProjectLinkStore = class {
22943
23599
  await ignoreLinkDirectory(this.repositoryRoot);
22944
23600
  }
22945
23601
  };
23602
+ async function readJson(path) {
23603
+ try {
23604
+ const value = JSON.parse(await readFile2(path, "utf8"));
23605
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
23606
+ } catch {
23607
+ return null;
23608
+ }
23609
+ }
23610
+ var isRecord3 = (value, keys) => typeof value === "object" && value !== null && Object.keys(value).sort().join(",") === [...keys].sort().join(",");
22946
23611
  async function ignoreLinkDirectory(root) {
22947
23612
  try {
22948
- await stat(join(root, ".git"));
23613
+ await stat(join2(root, ".git"));
22949
23614
  } catch {
22950
23615
  return;
22951
23616
  }
22952
- const file = join(root, ".gitignore");
23617
+ const file = join2(root, ".gitignore");
22953
23618
  let current = "";
22954
23619
  try {
22955
- current = await readFile(file, "utf8");
23620
+ current = await readFile2(file, "utf8");
22956
23621
  } catch {
22957
23622
  current = "";
22958
23623
  }
@@ -22965,7 +23630,7 @@ async function ignoreLinkDirectory(root) {
22965
23630
 
22966
23631
  // apps/product-cli/dist/repository-init.js
22967
23632
  import { createHash } from "node:crypto";
22968
- import { lstat as lstat2, open as open3, readFile as readFile2, readdir, stat as stat2 } from "node:fs/promises";
23633
+ import { lstat as lstat2, open as open4, readFile as readFile3, readdir as readdir3, stat as stat2 } from "node:fs/promises";
22969
23634
  import { dirname as dirname2, posix, resolve as resolve2 } from "node:path";
22970
23635
 
22971
23636
  // packages/contracts/dist/application-root.js
@@ -23933,7 +24598,7 @@ async function cronBlockers(framework, config, applicationDirectory, files) {
23933
24598
  ];
23934
24599
  }
23935
24600
  const modulePath = framework === "vite" ? "src/ohmyhost/companion.ts" : WORKER_MODULE_PATH;
23936
- const source = files.includes(modulePath) ? await readFile2(resolve2(applicationDirectory, modulePath), "utf8") : null;
24601
+ const source = files.includes(modulePath) ? await readFile3(resolve2(applicationDirectory, modulePath), "utf8") : null;
23937
24602
  if (framework !== "functions" && source !== null && !DEFAULT_EXPORT_PATTERN.test(source)) {
23938
24603
  return [defaultExportBlocker(modulePath)];
23939
24604
  }
@@ -23957,7 +24622,7 @@ async function storageClientBlockers(config, applicationDirectory, files) {
23957
24622
  const information = await stat2(absolute);
23958
24623
  if (!information.isFile() || information.size > MAXIMUM_SOURCE_BYTES)
23959
24624
  continue;
23960
- if (callsStorageClient(await readFile2(absolute, "utf8")))
24625
+ if (callsStorageClient(await readFile3(absolute, "utf8")))
23961
24626
  return [];
23962
24627
  }
23963
24628
  return [
@@ -23978,7 +24643,7 @@ async function databaseContractBlockers(config, applicationDirectory, files) {
23978
24643
  const information = await stat2(absolute);
23979
24644
  if (!information.isFile() || information.size > MAXIMUM_SOURCE_BYTES)
23980
24645
  continue;
23981
- const source = (await readFile2(absolute, "utf8")).replaceAll(INERT_SOURCE_PATTERN, " ");
24646
+ const source = (await readFile3(absolute, "utf8")).replaceAll(INERT_SOURCE_PATTERN, " ");
23982
24647
  if (DATABASE_PRIVATE_BINDING_PATTERN.test(source)) {
23983
24648
  blockers.push({
23984
24649
  code: "database_binding_private",
@@ -23997,7 +24662,7 @@ async function databaseContractBlockers(config, applicationDirectory, files) {
23997
24662
  async function workerModuleBlockers(framework, applicationDirectory, files) {
23998
24663
  if (framework !== "functions" || !files.includes(WORKER_MODULE_PATH))
23999
24664
  return [];
24000
- const source = await readFile2(resolve2(applicationDirectory, WORKER_MODULE_PATH), "utf8");
24665
+ const source = await readFile3(resolve2(applicationDirectory, WORKER_MODULE_PATH), "utf8");
24001
24666
  return DEFAULT_EXPORT_PATTERN.test(source) ? [] : [defaultExportBlocker(WORKER_MODULE_PATH)];
24002
24667
  }
24003
24668
  function migrationFilenameBlockers(files, configuredPath, provider) {
@@ -24024,7 +24689,7 @@ async function migrationSqlBlockers(applicationDirectory, files, configuredPath,
24024
24689
  }
24025
24690
  const migrations = await Promise.all(migrationPaths.map(async (path) => Object.freeze({
24026
24691
  path,
24027
- body: new Uint8Array(await readFile2(resolve2(applicationDirectory, root, path)))
24692
+ body: new Uint8Array(await readFile3(resolve2(applicationDirectory, root, path)))
24028
24693
  })));
24029
24694
  const blockers = [];
24030
24695
  for (const migration of migrations) {
@@ -24063,7 +24728,7 @@ async function readExistingConfiguration(repositoryRoot, repositoryFiles, applic
24063
24728
  if (!information.isFile() || information.size > MAXIMUM_CONFIGURATION_BYTES) {
24064
24729
  throw new RepositoryInitError("repository_configuration_invalid", "Existing ohmyhost.yaml must be one bounded regular file.");
24065
24730
  }
24066
- const configuration = await readFile2(configurationPath, "utf8");
24731
+ const configuration = await readFile3(configurationPath, "utf8");
24067
24732
  let parsed;
24068
24733
  try {
24069
24734
  parsed = parseOhmyhostConfigYaml(configuration);
@@ -24191,7 +24856,7 @@ async function tryReadPackageJson(root) {
24191
24856
  }
24192
24857
  function hasDirectSafeBuildCommand(packageJson, framework) {
24193
24858
  const scripts = packageJson["scripts"];
24194
- if (!isRecord2(scripts) || typeof scripts["build"] !== "string")
24859
+ if (!isRecord4(scripts) || typeof scripts["build"] !== "string")
24195
24860
  return false;
24196
24861
  const command = scripts["build"];
24197
24862
  if (command.includes("..") || /(?:^|\s)cd(?:\s|$)/u.test(command))
@@ -24239,7 +24904,7 @@ async function collectRepositoryFiles(root) {
24239
24904
  const directory = directories.pop();
24240
24905
  if (directory === void 0)
24241
24906
  break;
24242
- for (const entry of await readdir(resolve2(root, directory), { withFileTypes: true })) {
24907
+ for (const entry of await readdir3(resolve2(root, directory), { withFileTypes: true })) {
24243
24908
  const relative = directory === "" ? entry.name : posix.join(directory, entry.name);
24244
24909
  if (entry.isSymbolicLink()) {
24245
24910
  throw new RepositoryInitError("repository_symlink_forbidden", "Repository symlinks are forbidden");
@@ -24272,8 +24937,8 @@ async function readPackageJson(root) {
24272
24937
  throw new RepositoryInitError("package_manifest_invalid", "package.json is invalid");
24273
24938
  }
24274
24939
  try {
24275
- const value = JSON.parse(await readFile2(path, "utf8"));
24276
- if (!isRecord2(value))
24940
+ const value = JSON.parse(await readFile3(path, "utf8"));
24941
+ if (!isRecord4(value))
24277
24942
  throw new TypeError();
24278
24943
  return value;
24279
24944
  } catch {
@@ -24284,7 +24949,7 @@ function dependencyNames(packageJson) {
24284
24949
  const names = /* @__PURE__ */ new Set();
24285
24950
  for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
24286
24951
  const value = packageJson[field];
24287
- if (!isRecord2(value))
24952
+ if (!isRecord4(value))
24288
24953
  continue;
24289
24954
  for (const [name, version2] of Object.entries(value)) {
24290
24955
  if (typeof version2 === "string" && version2.length > 0)
@@ -24423,9 +25088,9 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
24423
25088
  });
24424
25089
  }
24425
25090
  const scripts = packageJson["scripts"];
24426
- const scriptRecord = isRecord2(scripts) ? scripts : {};
25091
+ const scriptRecord = isRecord4(scripts) ? scripts : {};
24427
25092
  if (framework === "functions") {
24428
- } else if (!isRecord2(scripts) || typeof scripts["build"] !== "string" || scripts["build"].length === 0) {
25093
+ } else if (!isRecord4(scripts) || typeof scripts["build"] !== "string" || scripts["build"].length === 0) {
24429
25094
  blockers.push({ code: "build_script_missing", message: "Declare a non-empty build script." });
24430
25095
  } else if (!hasDirectSafeBuildCommand(packageJson, framework)) {
24431
25096
  blockers.push({
@@ -24441,7 +25106,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
24441
25106
  });
24442
25107
  }
24443
25108
  if (framework === "nextjs") {
24444
- const dependencies = isRecord2(packageJson["dependencies"]) ? packageJson["dependencies"] : {};
25109
+ const dependencies = isRecord4(packageJson["dependencies"]) ? packageJson["dependencies"] : {};
24445
25110
  const nextBuild = scriptRecord["build"] === "next build" || scriptRecord["build"] === "next build --webpack";
24446
25111
  const nextPackageConfigured = nextBuild && compatibility.frameworks[0]?.classification !== "unsupported" && exactVersion(dependencies["react"]) && exactVersion(dependencies["react-dom"]) && frameworkConfigs.next !== null;
24447
25112
  const nextConfigured = nextPackageConfigured && frameworkConfigs.ambiguous.every((config) => config !== "open-next");
@@ -24466,7 +25131,7 @@ function exactVersion(value) {
24466
25131
  function dependencyVersion(packageJson, name) {
24467
25132
  for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
24468
25133
  const dependencies = packageJson[field];
24469
- if (!isRecord2(dependencies))
25134
+ if (!isRecord4(dependencies))
24470
25135
  continue;
24471
25136
  const version2 = dependencies[name];
24472
25137
  if (typeof version2 === "string" && version2.length > 0)
@@ -24537,7 +25202,7 @@ async function detectNextStandaloneOutput(root, framework, nextConfigPath) {
24537
25202
  const information = await stat2(path);
24538
25203
  if (!information.isFile() || information.size > MAXIMUM_CONFIGURATION_BYTES)
24539
25204
  return false;
24540
- return declaresStandaloneOutput(await readFile2(path, "utf8"));
25205
+ return declaresStandaloneOutput(await readFile3(path, "utf8"));
24541
25206
  }
24542
25207
  function declaresStandaloneOutput(source) {
24543
25208
  const tokens = sourceTokens(source);
@@ -24606,7 +25271,7 @@ async function detectTanStackRuntime(root, files, framework, packageJson, viteCo
24606
25271
  const information = await stat2(path);
24607
25272
  if (!information.isFile() || information.size > MAXIMUM_PACKAGE_BYTES)
24608
25273
  return "ambiguous";
24609
- const source = await readFile2(path, "utf8");
25274
+ const source = await readFile3(path, "utf8");
24610
25275
  const tokens = sourceTokens(source);
24611
25276
  if (isCompleteTanStackStatic(tokens))
24612
25277
  return "static";
@@ -24678,10 +25343,10 @@ function directSourceObjectProperty(tokens, start, end, name) {
24678
25343
  }
24679
25344
  return null;
24680
25345
  }
24681
- function matchingSourceDelimiter(tokens, start, open4, close) {
25346
+ function matchingSourceDelimiter(tokens, start, open5, close) {
24682
25347
  let depth = 0;
24683
25348
  for (let index = start; index < tokens.length; index += 1) {
24684
- if (tokens[index] === open4)
25349
+ if (tokens[index] === open5)
24685
25350
  depth += 1;
24686
25351
  if (tokens[index] === close)
24687
25352
  depth -= 1;
@@ -24715,7 +25380,7 @@ async function detectR2Storage(root, files) {
24715
25380
  const information = await stat2(path);
24716
25381
  if (!information.isFile() || information.size > MAXIMUM_CONFIGURATION_BYTES)
24717
25382
  continue;
24718
- const source = await readFile2(path, "utf8");
25383
+ const source = await readFile3(path, "utf8");
24719
25384
  if (/\br2_buckets\b/u.test(source))
24720
25385
  return true;
24721
25386
  }
@@ -24757,7 +25422,7 @@ function viteCompanionScaffold(files, capabilities) {
24757
25422
  async function createConfiguration(root, configuration) {
24758
25423
  let handle;
24759
25424
  try {
24760
- handle = await open3(resolve2(root, "ohmyhost.yaml"), "wx", 384);
25425
+ handle = await open4(resolve2(root, "ohmyhost.yaml"), "wx", 384);
24761
25426
  await handle.writeFile(configuration, "utf8");
24762
25427
  await handle.sync();
24763
25428
  } catch (error) {
@@ -24770,7 +25435,7 @@ async function createConfiguration(root, configuration) {
24770
25435
  await handle?.close();
24771
25436
  }
24772
25437
  }
24773
- function isRecord2(value) {
25438
+ function isRecord4(value) {
24774
25439
  return typeof value === "object" && value !== null && !Array.isArray(value);
24775
25440
  }
24776
25441
  function isNodeError(value) {
@@ -24778,11 +25443,8 @@ function isNodeError(value) {
24778
25443
  }
24779
25444
 
24780
25445
  // apps/product-cli/dist/composition.js
24781
- import { randomUUID } from "node:crypto";
24782
- import { join as join2 } from "node:path";
24783
-
24784
- // packages/workos-auth-contracts/dist/user-api-keys.js
24785
- var WORKOS_USER_API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{20,128}$/u;
25446
+ import { randomUUID as randomUUID2 } from "node:crypto";
25447
+ import { join as join3 } from "node:path";
24786
25448
 
24787
25449
  // packages/workos-auth-contracts/dist/device-flow.js
24788
25450
  var WORKOS_AUTHKIT_DEVICE_AUTHORIZATION_ENDPOINT = "https://api.workos.com/user_management/authorize/device";
@@ -25012,37 +25674,99 @@ function environmentToken(environment = {}) {
25012
25674
  throw new ProductApiAuthenticationError("invalid_environment_token");
25013
25675
  return value;
25014
25676
  }
25677
+ function environmentProfile(environment = {}) {
25678
+ const value = environment.OHMYHOST_PROFILE;
25679
+ if (value === void 0 || value === "")
25680
+ return void 0;
25681
+ if (!isProfileName(value))
25682
+ throw new ProductApiAuthenticationError("invalid_profile_binding");
25683
+ return value;
25684
+ }
25685
+ var profileRequest = (input, bound) => ({
25686
+ ...input?.name === void 0 ? {} : { name: input.name },
25687
+ ...bound === void 0 ? {} : { bound },
25688
+ ...input?.organizationId === void 0 ? {} : { organizationId: input.organizationId },
25689
+ ...input?.userId === void 0 ? {} : { userId: input.userId }
25690
+ });
25015
25691
  var createProductionAuthenticatedProductApi = async (signal, overrides = {}) => {
25016
25692
  const transport = await createAuthenticatedProductTransport(signal, overrides);
25017
25693
  return new GeneratedSdkProductApi(transport.baseUrl, transport.accessToken, transport.fetch);
25018
25694
  };
25019
25695
  var createAuthenticatedProductTransport = async (signal, overrides = {}) => {
25020
- const profile = resolveProductCliEnvironment(overrides.environment);
25696
+ const platform = resolveProductCliEnvironment(overrides.environment);
25021
25697
  const apiToken = environmentToken(overrides.environment);
25698
+ const bound = environmentProfile(overrides.environment);
25699
+ const baseUrl = overrides.productApiOrigin ?? platform.apiOrigin;
25022
25700
  const boundedFetch = createBoundedFetch(overrides.fetch ?? fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES);
25023
- if (apiToken !== void 0)
25701
+ const registry = overrides.profiles ?? new NativeKeyringProfileRegistry(void 0, platform.credentialService);
25702
+ const binding = overrides.request?.bindOrganization === true;
25703
+ const request = profileRequest(overrides.request, bound);
25704
+ if (apiToken !== void 0) {
25705
+ await verifyEnvironmentTokenContext({
25706
+ token: apiToken,
25707
+ request: binding ? profileRequest({}, bound) : request,
25708
+ registry,
25709
+ baseUrl,
25710
+ fetch: boundedFetch
25711
+ });
25024
25712
  return {
25025
- baseUrl: overrides.productApiOrigin ?? profile.apiOrigin,
25713
+ baseUrl,
25026
25714
  accessToken: apiToken,
25027
25715
  fetch: boundedFetch,
25028
- // A user API token carries a fixed scope; there is no session that could select a workspace.
25029
- workspace: void 0
25716
+ workspace: void 0,
25717
+ context: { credential: "environment_token", profile: null }
25030
25718
  };
25031
- const store = overrides.credentialStore ?? new NativeNapiKeyringCredentialStore(void 0, profile.credentialService);
25032
- const stored = await store.read();
25033
- if (stored === void 0)
25034
- throw new ProductApiAuthenticationError("authentication_required");
25719
+ }
25035
25720
  const now = overrides.now ?? (() => /* @__PURE__ */ new Date());
25036
25721
  const refresher = overrides.tokenRefresher ?? new PublicWorkosTokenRefresher(overrides.environment, overrides.fetch, now);
25037
- const credential = await currentCredential(stored, store, signal, now, refresher);
25722
+ const profiles = await loadProfiles(registry, {
25723
+ baseUrl,
25724
+ fetch: boundedFetch,
25725
+ signal,
25726
+ now,
25727
+ refresher
25728
+ });
25729
+ const selection = binding && request.organizationId !== void 0 ? selectProfileForOrganization(profiles, {
25730
+ ...request,
25731
+ organizationId: request.organizationId
25732
+ }) : selectProfile(profiles, request);
25733
+ if (selection.outcome !== "selected")
25734
+ throw profileSelectionError(selection, request, platform.commandPrefix, overrides.selector ?? "cli");
25735
+ const profile = selection.profile;
25736
+ const store = registry.credential(profile);
25737
+ const stored = await store.read();
25738
+ if (stored === void 0)
25739
+ throw missingLogin(profile, platform.commandPrefix);
25740
+ const credential = await currentCredential(stored, store, signal, now, refresher, () => registry.remove(profile));
25038
25741
  return {
25039
- baseUrl: overrides.productApiOrigin ?? profile.apiOrigin,
25742
+ baseUrl,
25040
25743
  accessToken: credential.accessToken,
25041
25744
  fetch: boundedFetch,
25042
- workspace: { store, refresher }
25745
+ workspace: { store, refresher, profiles: registry, profile },
25746
+ context: { credential: "profile", profile }
25747
+ };
25748
+ };
25749
+ var listSavedProfiles = async (signal, overrides = {}) => {
25750
+ const platform = resolveProductCliEnvironment(overrides.environment);
25751
+ const apiToken = environmentToken(overrides.environment);
25752
+ const bound = environmentProfile(overrides.environment);
25753
+ const registry = overrides.profiles ?? new NativeKeyringProfileRegistry(void 0, platform.credentialService);
25754
+ const now = overrides.now ?? (() => /* @__PURE__ */ new Date());
25755
+ const profiles = apiToken === void 0 ? await loadProfiles(registry, {
25756
+ baseUrl: overrides.productApiOrigin ?? platform.apiOrigin,
25757
+ fetch: createBoundedFetch(overrides.fetch ?? fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES),
25758
+ signal,
25759
+ now,
25760
+ refresher: overrides.tokenRefresher ?? new PublicWorkosTokenRefresher(overrides.environment, overrides.fetch, now)
25761
+ }) : await registry.list();
25762
+ return {
25763
+ profiles: profiles.map(publicProfile),
25764
+ bound: bound ?? null,
25765
+ environment_token: apiToken !== void 0
25043
25766
  };
25044
25767
  };
25045
- var currentCredential = async (stored, store, signal, now, tokenRefresher) => {
25768
+ var missingLogin = (profile, commandPrefix) => new ProfileSelectionError("authentication_required", 5, `The saved login ${profile.name} has no credential on this computer. Run '${commandPrefix} login${profile.organizationId === null ? "" : ` --organization ${profile.organizationId}`} --profile-name ${profile.name} --json'.`);
25769
+ var currentCredential = async (stored, store, signal, now, tokenRefresher, forget) => {
25046
25770
  const expiresAt = stored.expiresAt === void 0 ? NaN : Date.parse(stored.expiresAt);
25047
25771
  if (Number.isFinite(expiresAt) && expiresAt > now().getTime() + REQUEST_TIMEOUT_MS) {
25048
25772
  return stored;
@@ -25058,119 +25782,350 @@ var currentCredential = async (stored, store, signal, now, tokenRefresher) => {
25058
25782
  return carried;
25059
25783
  } catch (error) {
25060
25784
  if (error instanceof TokenRefreshRejectedError) {
25061
- await store.clear();
25785
+ await forget();
25062
25786
  throw new ProductApiAuthenticationError("authentication_required");
25063
25787
  }
25064
25788
  throw new ProductApiAuthenticationError("token_refresh_failed");
25065
25789
  }
25066
25790
  };
25791
+ var loadProfiles = async (registry, context) => {
25792
+ const legacy = registry.legacy();
25793
+ const stored = await legacy.read();
25794
+ if (stored !== void 0)
25795
+ await migrateLegacyLogin(registry, legacy, stored, context);
25796
+ return registry.list();
25797
+ };
25798
+ var migrateLegacyLogin = async (registry, legacy, stored, context) => {
25799
+ let credential;
25800
+ try {
25801
+ credential = await currentCredential(stored, legacy, context.signal, context.now, context.refresher, () => legacy.clear());
25802
+ } catch (error) {
25803
+ if (error instanceof ProductApiAuthenticationError && error.code === "authentication_required")
25804
+ return;
25805
+ throw error;
25806
+ }
25807
+ const api = new GeneratedSdkProductApi(context.baseUrl, credential.accessToken, context.fetch);
25808
+ let identity;
25809
+ try {
25810
+ identity = await api.getCurrentIdentity();
25811
+ } catch (error) {
25812
+ if (!isProblem(error, "unauthenticated"))
25813
+ throw error;
25814
+ return;
25815
+ }
25816
+ const userId = verifiedUserId(identity);
25817
+ const organizationId = parseCurrentIdentity(identity).organization_ids[0] ?? null;
25818
+ const organizationName = organizationId === null ? null : (await api.getAccountProfile()).organizations.find((organization) => organization.id === organizationId)?.name ?? null;
25819
+ const login = { userId, organizationId, organizationName };
25820
+ await registry.save({ name: defaultProfileName(await registry.list(), login), ...login }, credential);
25821
+ await legacy.clear();
25822
+ };
25823
+ var isProblem = (error, code) => typeof error === "object" && error !== null && "code" in error && error.code === code;
25824
+ var verifyEnvironmentTokenContext = async (input) => {
25825
+ const { request } = input;
25826
+ const named = request.name ?? request.bound;
25827
+ if (named === void 0 && request.organizationId === void 0 && request.userId === void 0)
25828
+ return null;
25829
+ const refuse = (target) => new ProfileSelectionError("environment_token_context_mismatch", 2, `${target} Unset OHMYHOST_TOKEN to use the saved login, or load the key of that account from its private env file. Nothing was sent.`);
25830
+ if (request.name !== void 0 && request.bound !== void 0 && request.name !== request.bound)
25831
+ throw refuse(`This call names profile ${request.name}, but the process is bound to OHMYHOST_PROFILE=${request.bound}.`);
25832
+ const profile = named === void 0 ? void 0 : (await input.registry.list()).find((saved) => saved.name === named);
25833
+ if (named !== void 0 && profile === void 0)
25834
+ throw refuse(`OHMYHOST_TOKEN overrides saved logins in this process, and no saved login is named ${named}.`);
25835
+ const { userId, organizationId } = await readTokenIdentity(input);
25836
+ const owner = `OHMYHOST_TOKEN belongs to user ${userId} in organization ${organizationId ?? "none"},`;
25837
+ if (profile !== void 0 && (profile.userId !== userId || profile.organizationId !== organizationId))
25838
+ throw refuse(`${owner} but this call names ${describeProfile(profile)}.`);
25839
+ if (request.organizationId !== void 0 && request.organizationId !== organizationId)
25840
+ throw refuse(`${owner} but this call targets organization ${request.organizationId}.`);
25841
+ if (request.userId !== void 0 && request.userId !== userId)
25842
+ throw refuse(`${owner} but this call targets user ${request.userId}.`);
25843
+ return { userId, organizationId };
25844
+ };
25845
+ var readTokenIdentity = async (input) => {
25846
+ const identity = parseCurrentIdentity(await new GeneratedSdkProductApi(input.baseUrl, input.token, input.fetch).getCurrentIdentity());
25847
+ return {
25848
+ userId: identity.actor_id.startsWith("user:") ? identity.actor_id.slice(5) : identity.actor_id,
25849
+ organizationId: identity.organization_ids[0] ?? null
25850
+ };
25851
+ };
25852
+ var commandOrganizationId = (command) => {
25853
+ if (command.kind === "organization-use" || command.kind === "login")
25854
+ return void 0;
25855
+ if (command.kind === "feedback-submit")
25856
+ return command.report.organization_id;
25857
+ return "organizationId" in command && typeof command.organizationId === "string" ? command.organizationId : void 0;
25858
+ };
25859
+ var commandProjectId = (command) => {
25860
+ if (command.kind === "managed-mail")
25861
+ return command.request.projectId;
25862
+ return "projectId" in command && typeof command.projectId === "string" ? command.projectId : void 0;
25863
+ };
25067
25864
  var runProductionCli = async (argv, environment, io, signal, overrides = {}) => {
25068
25865
  const diagnostics = new StderrDiagnosticSink(io);
25866
+ const written = (result) => {
25867
+ io.writeStdout(result.stdout);
25868
+ if (result.stderr.length > 0)
25869
+ io.writeStderr(result.stderr);
25870
+ return result.exitCode;
25871
+ };
25872
+ let args;
25873
+ let profileName;
25874
+ try {
25875
+ const extracted = extractProfileName(argv);
25876
+ args = extracted.argv;
25877
+ profileName = extracted.profileName;
25878
+ } catch {
25879
+ return written(invalidCommandFailure(recognizedCommandLabel(argv)));
25880
+ }
25881
+ const workingDirectory = overrides.workingDirectory ?? process.cwd();
25069
25882
  let command;
25883
+ let linked;
25884
+ let links = { links: [], legacy: null };
25070
25885
  try {
25071
- const parsed = await parseWithLinkedProject(argv, async () => {
25072
- const profile2 = resolveProductCliEnvironment(environment);
25073
- return readLinkedProjectId(join2(overrides.workingDirectory ?? process.cwd(), profile2.linkDirectory), profile2.apiOrigin);
25886
+ const parsed = await parseWithLinkedProject(args, async () => {
25887
+ let platform2;
25888
+ try {
25889
+ platform2 = resolveProductCliEnvironment(environment);
25890
+ } catch {
25891
+ return { outcome: "none" };
25892
+ }
25893
+ links = await readProjectLinks(join3(workingDirectory, platform2.linkDirectory), platform2.apiOrigin);
25894
+ let named = profileName;
25895
+ try {
25896
+ named ??= environmentProfile(environment);
25897
+ } catch {
25898
+ }
25899
+ const profiles = named === void 0 ? [] : await (overrides.profiles ?? new NativeKeyringProfileRegistry(void 0, platform2.credentialService)).list();
25900
+ return chooseLinkedProject(links, profiles.find((profile2) => profile2.name === named)?.organizationId ?? void 0);
25074
25901
  });
25075
25902
  command = parsed.command;
25076
- if (parsed.linkedProjectId !== null)
25077
- io.writeStderr(`ohmyhost: using linked project ${parsed.linkedProjectId}
25903
+ linked = parsed.linked;
25904
+ if (linked !== null)
25905
+ io.writeStderr(`ohmyhost: using linked project ${linked.projectId}
25078
25906
  `);
25079
25907
  } catch (error) {
25080
- const result2 = error instanceof InvalidCommandError ? invalidCommandFailure(recognizedCommandLabel(argv)) : errorDocument("unknown", 1, "internal_error", false, "Retry the command.");
25908
+ if (error instanceof LinkedProjectAmbiguousError)
25909
+ return written(errorDocument(recognizedCommandLabel(args), 2, "linked_project_selection_required", false, `This directory is linked to projects of several organizations (${error.organizationIds.join(", ")}). Pass --project ULID, or choose the login with --profile-name NAME; '${resolveProductCliEnvironment(environment).commandPrefix} profile list --json' shows the saved logins.`));
25910
+ if (error instanceof CredentialStoreUnavailableError)
25911
+ return written(errorDocument(recognizedCommandLabel(args), 3, "credential_store_unavailable", true, "Unlock or repair the selected credential store and retry."));
25912
+ const result = error instanceof InvalidCommandError ? invalidCommandFailure(recognizedCommandLabel(args)) : errorDocument("unknown", 1, "internal_error", false, "Retry the command.");
25081
25913
  if (!(error instanceof InvalidCommandError)) {
25082
25914
  diagnostics.record({
25083
- correlationId: randomUUID(),
25915
+ correlationId: randomUUID2(),
25084
25916
  category: "internal",
25085
25917
  subsystem: "parser",
25086
25918
  errorKind: "unexpected"
25087
25919
  });
25088
25920
  }
25089
- io.writeStdout(result2.stdout);
25090
- return result2.exitCode;
25921
+ return written(result);
25091
25922
  }
25092
25923
  if (command.kind === "init") {
25924
+ if (profileName !== void 0)
25925
+ return written(invalidCommandFailure("init"));
25093
25926
  return runOfflineInit(command, io, diagnostics);
25094
25927
  }
25095
25928
  if (command.kind === "help" || command.kind === "version") {
25096
- const result2 = cliMetadataResult(command.kind, command.kind === "help" ? command.topic : void 0);
25097
- io.writeStdout(result2.stdout);
25098
- return result2.exitCode;
25929
+ return written(cliMetadataResult(command.kind, command.kind === "help" ? command.topic : void 0));
25099
25930
  }
25100
- let profile;
25931
+ let platform;
25101
25932
  try {
25102
- profile = resolveProductCliEnvironment(environment);
25933
+ platform = resolveProductCliEnvironment(environment);
25103
25934
  } catch {
25104
- const result2 = errorDocument(recognizedCommandLabel(argv), 2, "invalid_environment", false, "Set OHMYHOST_ENVIRONMENT to development or production.");
25105
- io.writeStdout(result2.stdout);
25106
- return result2.exitCode;
25935
+ return written(errorDocument(recognizedCommandLabel(args), 2, "invalid_environment", false, "Set OHMYHOST_ENVIRONMENT to development or production."));
25107
25936
  }
25108
25937
  let apiToken;
25109
25938
  try {
25110
25939
  apiToken = environmentToken(environment);
25111
25940
  } catch {
25112
- const result2 = errorDocument(recognizedCommandLabel(argv), 2, "invalid_environment_token", false, "Set OHMYHOST_TOKEN to a valid user API key, or unset it to use interactive login.");
25113
- io.writeStdout(result2.stdout);
25114
- return result2.exitCode;
25941
+ return written(errorDocument(recognizedCommandLabel(args), 2, "invalid_environment_token", false, "Set OHMYHOST_TOKEN to a valid user API key, or unset it to use interactive login."));
25115
25942
  }
25116
- if (apiToken !== void 0 && (command.kind === "login" || command.kind === "logout")) {
25117
- const result2 = errorDocument(recognizedCommandLabel(argv), 2, "environment_token_active", false, "Unset OHMYHOST_TOKEN before changing interactive login credentials. Logout does not revoke a user API key.");
25118
- io.writeStdout(result2.stdout);
25119
- return result2.exitCode;
25120
- }
25121
- const selectedOverrides = {
25122
- ...overrides,
25123
- credentialStore: overrides.credentialStore ?? new NativeNapiKeyringCredentialStore(void 0, profile.credentialService)
25124
- };
25125
- if (command.kind === "logout" && !command.revoke) {
25126
- return runOfflineLogout(io, diagnostics, selectedOverrides);
25943
+ let bound;
25944
+ try {
25945
+ bound = environmentProfile(environment);
25946
+ } catch {
25947
+ return written(errorDocument(recognizedCommandLabel(args), 2, "invalid_profile_binding", false, `Set OHMYHOST_PROFILE to the name of a saved login ('${platform.commandPrefix} profile list --json'), or unset it.`));
25127
25948
  }
25128
- const productApiOrigin = profile.apiOrigin;
25949
+ if (apiToken !== void 0 && (command.kind === "login" || command.kind === "logout")) {
25950
+ return written(errorDocument(recognizedCommandLabel(args), 2, "environment_token_active", false, "Unset OHMYHOST_TOKEN before changing interactive login credentials. Logout does not revoke a user API key."));
25951
+ }
25952
+ const requiredToken = command.kind === "secret-set" ? command.environmentToken : void 0;
25953
+ const handedAccount2 = command.kind === "secret-set" ? requiredToken ?? command.profileAccount : void 0;
25954
+ if (requiredToken !== void 0 && apiToken === void 0)
25955
+ return written(errorDocument(recognizedCommandLabel(args), 2, "environment_token_required", false, `This command runs only with OHMYHOST_TOKEN holding a key of user ${requiredToken.userId} in organization ${requiredToken.organizationId}; a saved login cannot run it. Load that key from its private env file into this process and retry. Nothing was sent.`));
25956
+ const registry = overrides.profiles ?? new NativeKeyringProfileRegistry(void 0, platform.credentialService);
25957
+ const linkDirectory = join3(workingDirectory, platform.linkDirectory);
25958
+ const projectId = commandProjectId(command);
25959
+ const organizationId = handedAccount2?.organizationId ?? commandOrganizationId(command) ?? linked?.organizationId ?? (projectId === void 0 ? void 0 : linkedOrganization(links, projectId));
25960
+ const request = profileRequest({
25961
+ ...profileName === void 0 ? {} : { name: profileName },
25962
+ ...organizationId === void 0 || organizationId === null ? {} : { organizationId },
25963
+ ...handedAccount2 === void 0 ? {} : { userId: handedAccount2.userId }
25964
+ }, bound);
25129
25965
  const transport = new FetchWorkosPublicTransport(signal, REQUEST_TIMEOUT_MS);
25966
+ const now = () => /* @__PURE__ */ new Date();
25967
+ const refresher = new PublicWorkosTokenRefresher(environment);
25968
+ const boundedFetch = () => createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES);
25969
+ let profile;
25970
+ try {
25971
+ if (apiToken !== void 0) {
25972
+ if (command.kind !== "profile-list")
25973
+ await verifyEnvironmentTokenContext({
25974
+ token: apiToken,
25975
+ request,
25976
+ registry,
25977
+ baseUrl: platform.apiOrigin,
25978
+ fetch: boundedFetch()
25979
+ });
25980
+ if (linked !== null && linked.organizationId === null)
25981
+ await adoptLegacyLink({
25982
+ links,
25983
+ directory: linkDirectory,
25984
+ repositoryRoot: workingDirectory,
25985
+ apiOrigin: platform.apiOrigin,
25986
+ organizationId: (await readTokenIdentity({
25987
+ token: apiToken,
25988
+ baseUrl: platform.apiOrigin,
25989
+ fetch: boundedFetch()
25990
+ })).organizationId,
25991
+ accessToken: apiToken,
25992
+ fetch: boundedFetch(),
25993
+ actor: "OHMYHOST_TOKEN",
25994
+ commandPrefix: platform.commandPrefix
25995
+ });
25996
+ } else if (command.kind === "logout" && !command.revoke) {
25997
+ return await runOfflineLogout(registry, request, platform.commandPrefix, io, diagnostics);
25998
+ } else if (command.kind !== "login") {
25999
+ const profiles = await loadProfiles(registry, {
26000
+ baseUrl: platform.apiOrigin,
26001
+ fetch: boundedFetch(),
26002
+ signal,
26003
+ now,
26004
+ refresher
26005
+ });
26006
+ if (command.kind !== "profile-list") {
26007
+ const selection = command.kind === "organization-use" ? selectProfileForOrganization(profiles, {
26008
+ ...request,
26009
+ organizationId: command.organizationId
26010
+ }) : selectProfile(profiles, request);
26011
+ if (selection.outcome !== "selected")
26012
+ throw profileSelectionError(selection, request, platform.commandPrefix, "cli");
26013
+ profile = selection.profile;
26014
+ io.writeStderr(`ohmyhost: acting as ${describeProfile(profile)}
26015
+ `);
26016
+ if (linked !== null && linked.organizationId === null) {
26017
+ const selected2 = await createAuthenticatedProductTransport(signal, {
26018
+ environment,
26019
+ profiles: registry,
26020
+ request: { name: profile.name }
26021
+ });
26022
+ await adoptLegacyLink({
26023
+ links,
26024
+ directory: linkDirectory,
26025
+ repositoryRoot: workingDirectory,
26026
+ apiOrigin: platform.apiOrigin,
26027
+ organizationId: profile.organizationId,
26028
+ accessToken: selected2.accessToken,
26029
+ fetch: selected2.fetch,
26030
+ actor: `profile ${profile.name}`,
26031
+ commandPrefix: platform.commandPrefix
26032
+ });
26033
+ }
26034
+ }
26035
+ }
26036
+ } catch (error) {
26037
+ return written(compositionFailure(recognizedCommandLabel(args), error, signal, platform.commandPrefix));
26038
+ }
26039
+ const selected = profile;
26040
+ const productApiOrigin = platform.apiOrigin;
25130
26041
  const lifecycle = new GeneratedSdkSessionRevoker({
25131
26042
  environment,
25132
- credentialStore: selectedOverrides.credentialStore
26043
+ profiles: registry,
26044
+ ...selected === void 0 ? {} : { request: { name: selected.name } }
25133
26045
  });
25134
26046
  const cli = new ProductCli({
25135
26047
  ...apiToken === void 0 ? {} : { apiToken },
25136
- commandPrefix: profile.commandPrefix,
25137
- credentialStore: () => selectedOverrides.credentialStore,
26048
+ commandPrefix: platform.commandPrefix,
26049
+ profiles: registry,
26050
+ ...selected === void 0 ? {} : { profile: selected },
26051
+ // A login in a process bound to one saved login renews exactly that one.
26052
+ ...(profileName ?? bound) === void 0 ? {} : { profileName: profileName ?? bound },
26053
+ ...bound === void 0 ? {} : { profileBinding: bound },
25138
26054
  deviceFlow: new WorkosDeviceFlowClient({
25139
- clientId: profile.clientId,
26055
+ clientId: platform.clientId,
25140
26056
  scope: "openid offline_access",
25141
- verificationUri: profile.verificationUri
25142
- }, transport, { now: () => /* @__PURE__ */ new Date() }),
26057
+ verificationUri: platform.verificationUri
26058
+ }, transport, { now }),
25143
26059
  deviceAuthorizationPresenter: new StderrDeviceAuthorizationPresenter(io),
25144
- productApi: (accessToken) => new GeneratedSdkProductApi(productApiOrigin, accessToken, createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES)),
25145
- // Login receives read-only workspace and GitHub queries instead of the product surface.
25146
- accountProfile: async (accessToken) => await new GeneratedSdkProductApi(productApiOrigin, accessToken, createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES)).getAccountProfile(),
25147
- githubConnectionStatus: (accessToken, organizationId) => getGithubOrganizationConnection({ organization_id: organizationId }, {
26060
+ productApi: (accessToken) => new GeneratedSdkProductApi(productApiOrigin, accessToken, boundedFetch()),
26061
+ // Login receives read-only identity, workspace and GitHub queries instead of the product surface.
26062
+ accountProfile: async (accessToken) => await new GeneratedSdkProductApi(productApiOrigin, accessToken, boundedFetch()).getAccountProfile(),
26063
+ currentIdentity: async (accessToken) => await new GeneratedSdkProductApi(productApiOrigin, accessToken, boundedFetch()).getCurrentIdentity(),
26064
+ githubConnectionStatus: (accessToken, organizationId2) => getGithubOrganizationConnection({ organization_id: organizationId2 }, {
25148
26065
  client: createClient({
25149
26066
  baseUrl: productApiOrigin,
25150
26067
  auth: accessToken,
25151
26068
  throwOnError: true,
25152
- fetch: createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES)
26069
+ fetch: boundedFetch()
25153
26070
  }),
25154
26071
  signal
25155
26072
  }),
25156
- projectLinkStore: new OwnerOnlyProjectLinkStore(join2(process.cwd(), profile.linkDirectory), process.cwd()),
25157
- productApiOrigin: profile.apiOrigin,
25158
- tokenRefresher: new PublicWorkosTokenRefresher(environment),
26073
+ projectLinkStore: new OwnerOnlyProjectLinkStore(linkDirectory, workingDirectory),
26074
+ productApiOrigin,
26075
+ tokenRefresher: refresher,
25159
26076
  tokenRevoker: lifecycle,
25160
- now: () => /* @__PURE__ */ new Date(),
26077
+ now,
25161
26078
  waitUntil,
25162
26079
  lifecycleTimeoutMs: REQUEST_TIMEOUT_MS,
25163
- createCorrelationId: randomUUID,
26080
+ createCorrelationId: randomUUID2,
25164
26081
  diagnosticSink: diagnostics,
25165
26082
  operationEventSink: { write: (value) => io.writeStdout(value) },
25166
26083
  readSecretValue: overrides.readSecretValue ?? ((readSignal) => readSecretValueFromStandardInput(process.stdin, readSignal)),
25167
26084
  spawnPsql: runLocalPsql
25168
26085
  });
25169
- const result = await cli.runCommand(command, signal);
25170
- io.writeStdout(result.stdout);
25171
- if (result.stderr.length > 0)
25172
- io.writeStderr(result.stderr);
25173
- return result.exitCode;
26086
+ return written(await cli.runCommand(command, signal));
26087
+ };
26088
+ var adoptLegacyLink = async (input) => {
26089
+ const legacy = input.links.legacy;
26090
+ if (legacy === null)
26091
+ return;
26092
+ const refuse = () => new ProfileSelectionError("linked_project_organization_mismatch", 2, `This directory's link names project ${legacy.project_id}, which ${input.actor} cannot see${input.organizationId === null ? "" : ` in organization ${input.organizationId}`}. Nothing was sent to it. Pass --project ULID or --profile-name NAME for the account that owns it, or run '${input.commandPrefix} link' again for this account.`);
26093
+ let organizationId;
26094
+ try {
26095
+ const project = await getProject({ project_id: legacy.project_id }, {
26096
+ client: createClient({
26097
+ baseUrl: input.apiOrigin,
26098
+ auth: input.accessToken,
26099
+ fetch: input.fetch,
26100
+ throwOnError: true
26101
+ })
26102
+ });
26103
+ organizationId = project.organization_id;
26104
+ } catch (error) {
26105
+ if (isProblem(error, "resource_not_found") || isProblem(error, "forbidden"))
26106
+ throw refuse();
26107
+ throw error;
26108
+ }
26109
+ if (organizationId !== input.organizationId)
26110
+ throw refuse();
26111
+ await new OwnerOnlyProjectLinkStore(input.directory, input.repositoryRoot).save({
26112
+ version: 2,
26113
+ api_origin: input.apiOrigin,
26114
+ organization_id: organizationId,
26115
+ project_id: legacy.project_id,
26116
+ installation_id: legacy.installation_id,
26117
+ repository_full_name: legacy.repository_full_name
26118
+ });
26119
+ await removeLegacyProjectLink(input.directory);
26120
+ };
26121
+ var compositionFailure = (command, error, signal, commandPrefix) => {
26122
+ if (error instanceof ProfileSelectionError)
26123
+ return errorDocument(command, error.exitCode, error.code, false, error.suggestedAction);
26124
+ if (error instanceof ProfileLimitError)
26125
+ return errorDocument(command, 2, "profile_limit_reached", false, profileLimitAction(commandPrefix));
26126
+ if (error instanceof ProductApiAuthenticationError)
26127
+ return error.code === "authentication_required" ? errorDocument(command, 5, "authentication_required", false, `Run '${commandPrefix} login --json'.`) : errorDocument(command, 5, "token_refresh_failed", true, `Run '${commandPrefix} login --json' if retrying does not succeed.`);
26128
+ return classifyCliFailure(command, error, signal);
25174
26129
  };
25175
26130
  var runOfflineInit = async (command, io, diagnostics) => {
25176
26131
  try {
@@ -25208,7 +26163,7 @@ var runOfflineInit = async (command, io, diagnostics) => {
25208
26163
  } catch (error) {
25209
26164
  const known = error instanceof RepositoryInitError;
25210
26165
  diagnostics.record({
25211
- correlationId: randomUUID(),
26166
+ correlationId: randomUUID2(),
25212
26167
  category: known ? "configuration" : "internal",
25213
26168
  subsystem: "runtime",
25214
26169
  errorKind: known ? "invalid_response" : "unexpected"
@@ -25329,7 +26284,7 @@ var GeneratedSdkSessionRevoker = class {
25329
26284
  fetch: transport.fetch,
25330
26285
  throwOnError: true
25331
26286
  });
25332
- const result = await revokeCurrentSession({ "Idempotency-Key": randomUUID() }, { client: client2, signal });
26287
+ const result = await revokeCurrentSession({ "Idempotency-Key": randomUUID2() }, { client: client2, signal });
25333
26288
  if (result.revoked !== true)
25334
26289
  throw new LifecycleTransportError();
25335
26290
  } catch (error) {
@@ -25339,25 +26294,27 @@ var GeneratedSdkSessionRevoker = class {
25339
26294
  }
25340
26295
  }
25341
26296
  };
25342
- async function parseWithLinkedProject(argv, linkedProjectId) {
26297
+ var LinkedProjectAmbiguousError = class extends Error {
26298
+ organizationIds;
26299
+ constructor(organizationIds) {
26300
+ super("Several organizations' links apply");
26301
+ this.organizationIds = organizationIds;
26302
+ this.name = "LinkedProjectAmbiguousError";
26303
+ }
26304
+ };
26305
+ async function parseWithLinkedProject(argv, linkedProject) {
25343
26306
  try {
25344
- return { command: parseProductCliCommand(argv), linkedProjectId: null };
26307
+ return { command: parseProductCliCommand(argv), linked: null };
25345
26308
  } catch (error) {
25346
26309
  if (!(error instanceof InvalidCommandError) || argv.includes("--project"))
25347
26310
  throw error;
25348
- let projectId;
25349
- try {
25350
- projectId = await linkedProjectId();
25351
- } catch {
25352
- projectId = null;
25353
- }
25354
- if (projectId === null)
26311
+ const linked = await linkedProject();
26312
+ if (linked.outcome === "ambiguous")
26313
+ throw new LinkedProjectAmbiguousError(linked.organizationIds);
26314
+ if (linked.outcome === "none")
25355
26315
  throw error;
25356
26316
  try {
25357
- return {
25358
- command: parseProductCliCommand([...argv, "--project", projectId]),
25359
- linkedProjectId: projectId
25360
- };
26317
+ return { command: parseProductCliCommand([...argv, "--project", linked.projectId]), linked };
25361
26318
  } catch {
25362
26319
  throw error;
25363
26320
  }
@@ -25485,14 +26442,25 @@ var waitUntil = async (instant, signal) => {
25485
26442
  };
25486
26443
  var boundedSignal2 = (parent, timeoutMs) => AbortSignal.any([parent, AbortSignal.timeout(timeoutMs)]);
25487
26444
  var isAbortOrTimeout = (error) => error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError");
25488
- var runOfflineLogout = async (io, diagnostics, overrides) => {
26445
+ var runOfflineLogout = async (registry, request, commandPrefix, io, diagnostics) => {
25489
26446
  let result;
25490
26447
  try {
25491
- await (overrides.credentialStore ?? new NativeNapiKeyringCredentialStore()).clear();
25492
- result = resultDocument("logout", 0, "logged_out");
26448
+ const selection = selectProfile(await registry.list(), request);
26449
+ if (selection.outcome === "selected") {
26450
+ await registry.remove(selection.profile);
26451
+ result = resultDocument("logout", 0, "logged_out", {
26452
+ profile: publicProfile(selection.profile)
26453
+ });
26454
+ } else if (selection.outcome === "none" && request.name === void 0 && request.bound === void 0) {
26455
+ await registry.legacy().clear();
26456
+ result = resultDocument("logout", 0, "logged_out");
26457
+ } else {
26458
+ const failure2 = profileSelectionError(selection, request, commandPrefix, "cli");
26459
+ result = errorDocument("logout", failure2.exitCode, failure2.code, false, failure2.suggestedAction);
26460
+ }
25493
26461
  } catch {
25494
26462
  diagnostics.record({
25495
- correlationId: randomUUID(),
26463
+ correlationId: randomUUID2(),
25496
26464
  category: "credential_store",
25497
26465
  subsystem: "credential_store",
25498
26466
  errorKind: "unavailable"
@@ -25533,9 +26501,9 @@ var readSecretValueFromStandardInput = async (input, signal) => {
25533
26501
  }
25534
26502
  };
25535
26503
  var invalidCommandFailure = (command) => errorDocument(command, 2, "invalid_command", false, "Check 'ohmyhost --help'.");
25536
- var resultDocument = (command, exitCode, status) => ({
26504
+ var resultDocument = (command, exitCode, status, body = {}) => ({
25537
26505
  exitCode,
25538
- stdout: `${JSON.stringify({ version: 1, command, status })}
26506
+ stdout: `${JSON.stringify({ version: 1, command, status, ...body })}
25539
26507
  `,
25540
26508
  stderr: ""
25541
26509
  });
@@ -25558,15 +26526,26 @@ var errorDocument = (command, exitCode, code, retryable, suggestedAction) => ({
25558
26526
  export {
25559
26527
  UserTokenFileError,
25560
26528
  prepareUserTokenFile,
26529
+ ProfileNameConflictError,
26530
+ ProfileLimitError,
26531
+ isProfileName,
26532
+ isProfileUserId,
26533
+ sameLogin,
26534
+ profileIndexDirectory,
26535
+ NativeKeyringProfileRegistry,
26536
+ ProfileIndexFiles,
26537
+ selectProfile,
26538
+ selectProfileForOrganization,
26539
+ defaultProfileName,
26540
+ publicProfile,
26541
+ profileLimitAction,
26542
+ ProfileSelectionError,
26543
+ profileSelectionError,
26544
+ describeProfile,
26545
+ extractProfileName,
25561
26546
  InvalidCommandError,
25562
26547
  recognizedCommandLabel,
25563
26548
  parseProductCliCommand,
25564
- InteractiveSessionRequiredError,
25565
- chooseWorkspace,
25566
- selectWorkspace,
25567
- selectedWorkspaceId,
25568
- workspaceOf,
25569
- publicWorkspace,
25570
26549
  parsePaidDomainPlan,
25571
26550
  parsePaidDomain,
25572
26551
  parseCloudflareDnsAuthorization,
@@ -25578,6 +26557,15 @@ export {
25578
26557
  parseOperationEvent,
25579
26558
  parseSafeProblem,
25580
26559
  parseOperation,
26560
+ InteractiveSessionRequiredError,
26561
+ ProfileOrganizationFixedError,
26562
+ AccountVerificationError,
26563
+ chooseWorkspace,
26564
+ verifiedUserId,
26565
+ assertVerifiedLogin,
26566
+ selectWorkspace,
26567
+ workspaceOf,
26568
+ publicWorkspace,
25581
26569
  NetworkTransportError,
25582
26570
  LifecycleTransportError,
25583
26571
  TokenRefreshRejectedError,
@@ -25587,8 +26575,13 @@ export {
25587
26575
  PsqlUnavailableError,
25588
26576
  cliMetadataResult,
25589
26577
  ProductCli,
26578
+ ACCOUNT_SENSITIVE_PROBLEMS,
26579
+ classifyCliFailure,
25590
26580
  GeneratedSdkProductApi,
25591
- readLinkedProjectId,
26581
+ readProjectLinks,
26582
+ chooseLinkedProject,
26583
+ linkedOrganization,
26584
+ removeLegacyProjectLink,
25592
26585
  OwnerOnlyProjectLinkStore,
25593
26586
  RepositoryInitError,
25594
26587
  RepositoryInitBlockedError,
@@ -25598,10 +26591,13 @@ export {
25598
26591
  resolveProductCliEnvironment,
25599
26592
  createProductionAuthenticatedProductApi,
25600
26593
  createAuthenticatedProductTransport,
26594
+ listSavedProfiles,
26595
+ loadProfiles,
25601
26596
  runProductionCli,
25602
26597
  StderrDeviceAuthorizationPresenter,
25603
26598
  PublicWorkosTokenRefresher,
25604
26599
  GeneratedSdkSessionRevoker,
26600
+ LinkedProjectAmbiguousError,
25605
26601
  parseWithLinkedProject,
25606
26602
  readBoundedResponseBody,
25607
26603
  createBoundedFetch,