{"version":3,"file":"keys.mjs","names":[],"sources":["../../src/handlers/keys.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Effect } from \"effect\";\nimport { parseKey } from \"near-kit\";\nimport {\n  DEFAULT_DEPLOY_KEY_EXPIRES_IN_SECONDS,\n  DEFAULT_LOGIN_EXPIRES_IN_SECONDS,\n  type LoginResult,\n  type LogoutResult,\n} from \"../contract\";\nimport { DeviceLoginService, openInBrowser } from \"../services/auth/device-login\";\nimport { loginOutlayerCli } from \"../services/auth/outlayer\";\nimport { mintPublishKey } from \"../services/auth/publish-key\";\nimport type { SessionCredential } from \"../services/auth/session\";\nimport { SessionStore } from \"../services/auth/session-store\";\nimport { SiteClient, type SiteClientShape } from \"../services/auth/site-client\";\nimport type { ResolutionSession } from \"../services/resolution/session\";\nimport { ResolutionSessionService } from \"../services/resolution/session\";\nimport { getNetworkIdForAccount } from \"../utils/network\";\nimport { colors } from \"../utils/theme\";\nimport type { BosBuilder } from \"./shared\";\n\nexport function registerKeys(builder: BosBuilder) {\n  return {\n    login: builder.login.effect(function* (_opts, input) {\n      const session = yield* ResolutionSessionService;\n      const deviceLogin = yield* DeviceLoginService;\n      const sessions = yield* SessionStore;\n      const staging = input.env === \"staging\";\n      const config = session?.config;\n      const account = staging\n        ? (config?.staging?.account ?? config?.account ?? \"\")\n        : (config?.account ?? \"\");\n      const gateway = staging\n        ? (config?.staging?.domain ?? config?.domain ?? \"\")\n        : (config?.domain ?? \"\");\n      const expiresIn =\n        input.expiresIn ??\n        (input.key ? DEFAULT_DEPLOY_KEY_EXPIRES_IN_SECONDS : DEFAULT_LOGIN_EXPIRES_IN_SECONDS);\n\n      const siteUrl = resolveLoginSiteUrl(input.site, session, staging);\n      let deployKey: { account: string; gateway: string } | undefined;\n      if (input.key) {\n        if (!account || !gateway) {\n          return {\n            status: \"error\" as const,\n            siteUrl: siteUrl,\n            error:\n              \"Deploy keys scope to the authored config's account and domain — set both in bos.config.json (or drop --key for a plain login)\",\n          } satisfies LoginResult;\n        }\n        deployKey = { account, gateway };\n      }\n\n      const login = yield* deviceLogin\n        .start({\n          siteUrl,\n          device: input.device,\n          account: account || undefined,\n          custody: input.custody ? { network: getNetworkIdForAccount(account) } : undefined,\n          expiresIn,\n          deployKey,\n        })\n        .pipe(\n          Effect.catchIf(\n            () => true,\n            (error: Error) => Effect.succeed({ startFailed: error.message } as const),\n          ),\n        );\n      if (\"startFailed\" in login) {\n        return {\n          status: \"error\" as const,\n          siteUrl: input.site ?? \"\",\n          error: login.startFailed,\n        } satisfies LoginResult;\n      }\n\n      console.log();\n      console.log(`  One-time code: ${colors.cyan(login.userCode)}`);\n      yield* Effect.catchIf(\n        Effect.tryPromise(() => openInBrowser(login.verificationUrl)),\n        () => true,\n        (error) => Effect.sync(() => console.log(colors.yellow(`  ⚠ ${(error as Error).message}`))),\n      );\n      console.log();\n      console.log(`  Waiting for approval at ${colors.dim(login.verificationUrl)}…`);\n\n      return yield* Effect.tryPromise({\n        try: async () => {\n          const approval = await login.waitForApproval();\n\n          if (!approval.apiKey) {\n            return {\n              status: \"error\" as const,\n              siteUrl,\n              loginUrl: login.verificationUrl,\n              error: \"Login approved but no CLI credential was created\",\n            } satisfies LoginResult;\n          }\n\n          const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();\n          const configDir = session?.root ?? process.cwd();\n          const existingSession = sessions.read(configDir);\n          sessions.write(configDir, {\n            version: 1,\n            credential: {\n              kind: \"session\",\n              apiKey: approval.apiKey.key,\n              apiKeyId: approval.apiKey.id,\n              accountId: approval.accountId,\n              label: input.device ?? siteUrl,\n              siteUrl,\n              createdAt: new Date().toISOString(),\n              expiresAt,\n            },\n            publishKey: null,\n            delegateKey: null,\n            custodyWallet: approval.custodyWallet ?? existingSession?.custodyWallet ?? null,\n          });\n\n          let warning: string | null = null;\n          if (input.custody && !approval.custodyWallet) {\n            warning =\n              \"Custody wallet credential could not be fetched after approval. Re-run `bos login --custody` — the wallet is kept server-side.\";\n          }\n          if (account && approval.accountId && approval.accountId !== account) {\n            warning = warning\n              ? `${warning} Logged in as ${approval.accountId}, but the authored config account is ${account}. Publishes will use the configured account.`\n              : `Logged in as ${approval.accountId}, but the authored config account is ${account}. Publishes will use the configured account.`;\n          }\n\n          let publishKey: LoginResult[\"publishKey\"] = null;\n          if (input.key) {\n            try {\n              publishKey = await exportPublishKey(sessions, account, input.registry);\n            } catch (error) {\n              warning = `Session stored, but publish-key export failed: ${\n                error instanceof Error ? error.message : \"unknown error\"\n              }`;\n            }\n          }\n\n          let outlayerCli: LoginResult[\"outlayerCli\"] = null;\n          const custodyWallet = approval.custodyWallet;\n          if (input.custody && custodyWallet) {\n            outlayerCli = await loginOutlayerCli(custodyWallet);\n            if (outlayerCli.status === \"error\") {\n              const failure = `OutLayer CLI login failed: ${outlayerCli.detail}`;\n              warning = warning ? `${warning} ${failure}` : failure;\n            }\n          }\n\n          return {\n            status: \"logged-in\" as const,\n            siteUrl,\n            accountId: approval.accountId,\n            expiresAt,\n            loginUrl: login.verificationUrl,\n            publishKey,\n            deployKey:\n              deployKey && approval.apiKey\n                ? {\n                    key: approval.apiKey.key,\n                    account: deployKey.account,\n                    gateway: deployKey.gateway,\n                  }\n                : null,\n            custodyWallet: approval.custodyWallet\n              ? {\n                  walletId: approval.custodyWallet.walletId,\n                  nearAccountId: approval.custodyWallet.nearAccountId,\n                  network: approval.custodyWallet.network,\n                  handoffUrl: approval.custodyWallet.handoffUrl,\n                }\n              : null,\n            outlayerCli,\n            warning,\n          } satisfies LoginResult;\n        },\n        catch: (error) =>\n          ({\n            status: \"error\" as const,\n            siteUrl: input.site ?? \"\",\n            error: error instanceof Error ? error.message : \"Unknown error\",\n          }) satisfies LoginResult,\n      });\n    }),\n\n    logout: builder.logout.effect(function* (_opts, input) {\n      const session = yield* ResolutionSessionService;\n      const siteClient = yield* SiteClient;\n      const sessions = yield* SessionStore;\n      const configDir = input.configDir ?? session?.root ?? process.cwd();\n\n      return yield* Effect.tryPromise({\n        try: () => logoutBody({ siteClient, sessions, session }, configDir),\n        catch: (error) =>\n          ({\n            status: \"error\" as const,\n            revokedApiKey: false,\n            removedPublishKey: false,\n            error: error instanceof Error ? error.message : \"Unknown error\",\n          }) satisfies LogoutResult,\n      });\n    }),\n  };\n}\n\nasync function logoutBody(\n  deps: {\n    siteClient: SiteClientShape;\n    sessions: SessionStore[\"Service\"];\n    session: ResolutionSession | null;\n  },\n  configDir: string,\n): Promise<LogoutResult> {\n  const { siteClient, sessions, session } = deps;\n  const stored = sessions.read(configDir);\n  let revokedApiKey = false;\n  let removedPublishKey = false;\n  let warning: string | null = null;\n\n  const credential = stored?.credential;\n  if (credential) {\n    revokedApiKey = await revokeApiKey(siteClient, credential).catch(() => false);\n    if (!revokedApiKey) {\n      warning =\n        \"Could not revoke the API key remotely — revoke it manually under Settings → API keys.\";\n    }\n  }\n\n  if (stored?.publishKey) {\n    const account = stored.credential?.accountId ?? session?.config?.account ?? \"\";\n    sessions.removePublishedKeyFile(\n      stored.publishKey.network,\n      account,\n      stored.publishKey.publicKey,\n    );\n    removedPublishKey = true;\n    warning = warning\n      ? `${warning} Re-run ${colors.cyan(\"bos login --key\")} to re-export a publish key.`\n      : \"Removed the exported publish key. Re-run bos login --key to re-export one.\";\n  }\n\n  sessions.delete(configDir);\n  return {\n    status: \"logged-out\" as const,\n    revokedApiKey,\n    removedPublishKey,\n    warning,\n  } satisfies LogoutResult;\n}\n\nfunction resolveLoginSiteUrl(\n  siteInput: string | undefined,\n  session: ResolutionSession | null,\n  staging: boolean,\n): string {\n  if (siteInput) {\n    return siteInput.replace(/\\/+$/, \"\");\n  }\n  if (staging) {\n    const stagingDomain = session?.config?.staging?.domain ?? session?.config?.domain;\n    return stagingDomain ? `https://${stagingDomain}` : (session?.runtime?.ui?.url ?? \"\");\n  }\n  const devUiUrl = session?.runtime?.ui?.url;\n  if (devUiUrl && /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)/.test(devUiUrl)) {\n    return devUiUrl.replace(/\\/+$/, \"\");\n  }\n  const domain = session?.config?.domain;\n  return domain ? `https://${domain}` : \"\";\n}\n\n/** `login --key`'s export: the shared mint, then the near-cli-rs keystore\n * handoff (~/.near-credentials) with a 0600 permission fix-up. */\nasync function exportPublishKey(\n  sessions: { nearCredentialsPath: (network: \"mainnet\" | \"testnet\", account: string) => string },\n  account: string,\n  registry: string | undefined,\n): Promise<NonNullable<LoginResult[\"publishKey\"]>> {\n  const minted = await mintPublishKey({ account, registry });\n\n  const { FileKeyStore } = await import(\"near-kit/keys/file\");\n  const keyStore = new FileKeyStore(\"~/.near-credentials\", minted.network);\n  await keyStore.add(account, parseNearPrivateKey(minted.keyPair.privateKey));\n  try {\n    const { statSync, chmodSync } = await import(\"node:fs\");\n    const credentialPath = sessions.nearCredentialsPath(minted.network, account);\n    if (statSync(credentialPath).mode & 0o077) {\n      chmodSync(credentialPath, 0o600);\n    }\n  } catch {}\n\n  return {\n    publicKey: minted.keyPair.publicKey,\n    network: minted.network,\n    contract: minted.contract,\n    exportedTo: sessions.nearCredentialsPath(minted.network, account),\n  };\n}\n\nfunction parseNearPrivateKey(privateKey: string): ReturnType<typeof parseKey> {\n  return parseKey(privateKey);\n}\n\nasync function revokeApiKey(\n  siteClient: SiteClientShape,\n  credential: SessionCredential,\n): Promise<boolean> {\n  if (!credential.apiKey || !credential.siteUrl) return false;\n  try {\n    const response = await siteClient.authPost(\n      credential.siteUrl,\n      \"/api-key/delete\",\n      { keyId: credential.apiKeyId },\n      undefined,\n      { \"x-api-key\": credential.apiKey },\n      AbortSignal.timeout(10_000),\n    );\n    return response.status >= 200 && response.status < 300;\n  } catch {\n    return false;\n  }\n}\n"],"mappings":"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"}